6 Komitmen 922ed6c829 ... 25a74cf281

Pembuat SHA1 Pesan Tanggal
  steven_roc 25a74cf281 fix27 v44: bug fixes + complete README with MNN/HyperLPR3 guide 22 jam lalu
  steven_roc b71a3c4a01 initial: uploaded source code 22 jam lalu
  steven_roc 922ed6c829 docs: add detailed MNN and HyperLPR3 installation guide 1 hari lalu
  steven_roc 4a277f2ca0 docs: update README.md and CHANGELOG.md 1 hari lalu
  steven_roc 6bb408c0ab fix27 v44: bug fixes from code review 1 hari lalu
  steven_roc d8ac0cc3df Initial commit 4 hari lalu
48 mengubah file dengan 86 tambahan dan 13048 penghapusan
  1. 0 1
      CMakeLists.txt
  2. 0 205
      assets/web/frpc.html
  3. 0 1185
      assets/web/help.html
  4. 0 284
      assets/web/index.bak.html
  5. 0 310
      assets/web/index.bak2.html
  6. 0 317
      assets/web/index.bak3.html
  7. 0 317
      assets/web/index.bak4..html
  8. 0 317
      assets/web/index.bak4.html
  9. 0 475
      assets/web/index.bak5.html
  10. 0 713
      assets/web/index.html
  11. 0 284
      assets/web/indexbak1.html
  12. 0 310
      assets/web/indexbak2.html
  13. 0 317
      assets/web/indexbak3.html
  14. 0 264
      assets/web/login.html
  15. 0 167
      assets/web/logs.html
  16. 0 129
      assets/web/ssh_keys.html
  17. 0 174
      assets/web/system.html
  18. 0 381
      assets/web/video.ba3.html
  19. 0 381
      assets/web/video.bak4.html
  20. 0 407
      assets/web/video.html
  21. 0 381
      assets/web/video3del.html
  22. 0 381
      assets/web/videobak3.html
  23. 0 751
      assets/web/weight.html
  24. 0 431
      config/config一号门.ini
  25. 0 431
      config/config二号门.ini
  26. 0 335
      scripts/deploy.sh
  27. 0 214
      scripts/reset_passwords.py
  28. 0 30
      scripts/setup_frpc_sudoers.sh
  29. 0 1
      src/auth_middleware.cpp
  30. 0 14
      src/common.cpp
  31. 4 21
      src/common.h
  32. 4 58
      src/config.cpp
  33. 0 866
      src/database.cpp
  34. 1 114
      src/database.h
  35. 1 13
      src/feishu_client.cpp
  36. 1 2
      src/feishu_client.h
  37. 17 158
      src/main.cpp
  38. 1 3
      src/network_client.cpp
  39. 2 33
      src/plate_recognizer.cpp
  40. 0 785
      src/plate_recognizer1.cpp
  41. 2 44
      src/station_lock.cpp
  42. 0 9
      src/utils.cpp
  43. 43 337
      src/web_server.cpp
  44. 0 3
      src/web_server.h
  45. 0 351
      src/weight_dispatcher.cpp
  46. 0 95
      src/weight_dispatcher.h
  47. 10 209
      src/weight_scale.cpp
  48. 0 40
      src/weight_scale.h

+ 0 - 1
CMakeLists.txt

@@ -266,7 +266,6 @@ set(SOURCES
     ${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/weight_dispatcher.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/src/mqtt_client.cpp
     # ✅ fix24 功能一:登录认证系统
     ${CMAKE_CURRENT_SOURCE_DIR}/src/bcrypt/bcrypt.c

+ 0 - 205
assets/web/frpc.html

@@ -1,205 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>frpc 管理 - PlateRecApp</title>
-<style>
-*{margin:0;padding:0;box-sizing:border-box}
-body{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
-.header{background:#1e293b;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155}
-.header h1{font-size:20px;font-weight:600;color:#f8fafc}
-.nav{display:flex;gap:16px;align-items:center}
-.nav a{color:#60a5fa;text-decoration:none;font-size:14px;padding:6px 12px;border-radius:6px;transition:background .2s}
-.nav a:hover,.nav a.active{background:#334155}
-.container{max-width:1200px;margin:0 auto;padding:20px}
-.tabs{display:flex;gap:8px;margin-bottom:20px}
-.tab{padding:8px 16px;background:#1e293b;border:1px solid #334155;border-radius:8px;cursor:pointer;color:#94a3b8;font-size:14px;transition:all .2s}
-.tab.active{background:#3b82f6;color:#fff;border-color:#3b82f6}
-.panel{display:none}.panel.active{display:block}
-.card{background:#1e293b;border-radius:12px;padding:20px;border:1px solid #334155;margin-bottom:16px}
-.card-title{font-size:15px;font-weight:600;margin-bottom:12px;color:#f8fafc}
-.status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:8px}
-.status-dot.online{background:#4ade80}.status-dot.offline{background:#f87171}
-table{width:100%;border-collapse:collapse;font-size:14px}
-th{text-align:left;padding:8px 12px;color:#94a3b8;border-bottom:1px solid #334155;font-weight:500}
-td{padding:8px 12px;border-bottom:1px solid #1e293b}
-.btn{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s}
-.btn-primary{background:#3b82f6;color:#fff}.btn-primary:hover{background:#2563eb}
-.btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c}
-.btn-success{background:#16a34a;color:#fff}.btn-success:hover{background:#15803d}
-.btn-group{display:flex;gap:8px;margin-top:12px}
-textarea{width:100%;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:12px;font-family:'Fira Code',monospace;font-size:13px;resize:vertical;min-height:400px;line-height:1.5}
-textarea:focus{outline:none;border-color:#3b82f6}
-.ssh-frame{width:100%;height:500px;border:1px solid #334155;border-radius:8px}
-.msg{padding:10px 14px;border-radius:8px;margin-top:10px;font-size:14px;display:none}
-.msg.success{display:block;background:#14532d;color:#86efac;border:1px solid #16a34a}
-.msg.error{display:block;background:#7f1d1d;color:#fca5a5;border:1px solid #dc2626}
-.ssh-link{display:inline-flex;align-items:center;gap:8px;padding:10px 20px;background:#1e293b;border:1px solid #334155;border-radius:8px;color:#60a5fa;text-decoration:none;font-size:14px;transition:all .2s}
-.ssh-link:hover{background:#334155;border-color:#60a5fa}
-
-    @media (max-width: 768px) {
-        .header { flex-direction: column; gap: 8px; padding: 12px 16px; }
-        .header h1 { font-size: 16px; }
-        .nav { flex-wrap: wrap; gap: 8px; justify-content: center; }
-        .nav a { font-size: 12px; padding: 4px 8px; }
-        .container { padding: 12px; }
-        .tabs { flex-wrap: wrap; }
-        .tab { flex: 1; min-width: 80px; text-align: center; font-size: 13px; padding: 6px 10px; }
-        .card { padding: 14px; }
-        table { font-size: 12px; display: block; overflow-x: auto; white-space: nowrap; }
-        th, td { padding: 6px 8px; }
-        .btn-group { flex-wrap: wrap; }
-        .btn { flex: 1; min-width: 100px; text-align: center; }
-        textarea { min-height: 200px; font-size: 12px; }
-        .ssh-frame { height: 350px; }
-    }
-    @media (max-width: 480px) {
-        .card-title { font-size: 13px; }
-        .btn { font-size: 12px; padding: 6px 12px; }
-    }
-    </style>
-</head>
-<body>
-<div class="header">
-    <h1>🔧 frpc 管理</h1>
-    <div class="nav">
-        <a href="/">监控台</a>
-        <a href="/video">视频</a>
-        <a href="/config">配置</a>
-        <a href="/system">系统</a>
-        <a href="/frpc" class="active">frpc</a>
-        <a href="/ssh-keys">SSH</a>
-        <a href="/logs">日志</a>
-        <a href="/help">帮助</a>
-    </div>
-</div>
-<div class="container">
-    <div class="tabs">
-        <div class="tab active" onclick="switchTab('status')">隧道状态</div>
-        <div class="tab" onclick="switchTab('config')">配置编辑</div>
-        <div class="tab" onclick="switchTab('ssh')">SSH 终端</div>
-    </div>
-
-    <!-- 隧道状态 -->
-    <div id="panel-status" class="panel active">
-        <div class="card">
-            <div class="card-title">
-                <span class="status-dot" id="svc-status"></span>
-                frpc 服务状态
-                <button class="btn btn-primary" style="float:right" onclick="refreshStatus()">🔄 刷新</button>
-            </div>
-            <table>
-                <thead><tr><th>名称</th><th>类型</th><th>本地地址</th><th>远程地址</th><th>状态</th></tr></thead>
-                <tbody id="tunnel-list"><tr><td colspan="5" style="color:#64748b">加载中...</td></tr></tbody>
-            </table>
-        </div>
-        <div class="btn-group">
-            <button class="btn btn-danger" onclick="restartFrpc()">⚡ 重启 frpc</button>
-        </div>
-        <div id="status-msg" class="msg"></div>
-    </div>
-
-    <!-- 配置编辑 -->
-    <div id="panel-config" class="panel">
-        <div class="card">
-            <div class="card-title">frpc.toml 配置文件
-                <button class="btn btn-primary" style="float:right" onclick="saveConfig()">💾 保存配置</button>
-            </div>
-            <textarea id="config-editor" spellcheck="false"></textarea>
-        </div>
-        <div class="btn-group">
-            <button class="btn btn-success" onclick="saveAndRestart()">💾 保存并重启</button>
-            <button class="btn btn-primary" onclick="loadConfig()">🔄 重新加载</button>
-        </div>
-        <div id="config-msg" class="msg"></div>
-    </div>
-
-    <!-- SSH 终端 -->
-    <div id="panel-ssh" class="panel">
-        <div class="card">
-            <div class="card-title">Web SSH 终端 (ttyd)</div>
-            <p style="color:#94a3b8;font-size:14px;margin-bottom:12px">通过 ttyd 提供的 Web SSH 终端直接连接到服务器。</p>
-            <a class="ssh-link" id="ssh-external-link" href="#" target="_blank">
-                🖥️ 在新窗口打开 SSH 终端
-            </a>
-            <div style="margin-top:16px">
-                <iframe id="ssh-frame" class="ssh-frame" src="" style="display:none"></iframe>
-                <button class="btn btn-primary" style="margin-top:8px" onclick="toggleSSH()">🔌 连接 SSH 终端</button>
-            </div>
-        </div>
-    </div>
-</div>
-<script>
-function getCookie(n){var m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}function authFetch(u,o){o=o||{};var h=o.headers||{};if(!(h instanceof Headers)){var nh=new Headers();for(var k in h)nh.set(k,h[k]);h=nh;}if(!h.has("X-CSRF-Token")){var t=getCookie("csrf_token");if(t)h.set("X-CSRF-Token",t);}o.headers=h;return fetch(u,o).then(function(r){if(r.status===401){window.location.href="/login?redirect="+encodeURIComponent(window.location.pathname);return new Promise(function(){});}return r;});}
-let sshConnected = false;
-function switchTab(name){
-    document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['status','config','ssh'][i]===name));
-    document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
-    document.getElementById('panel-'+name).classList.add('active');
-    if(name==='config'&&!document.getElementById('config-editor').value) loadConfig();
-    if(name==='status') refreshStatus();
-}
-function showMsg(id,text,ok){
-    let el=document.getElementById(id);
-    el.className='msg '+(ok?'success':'error');
-    el.textContent=text;
-    setTimeout(()=>el.className='msg',5000);
-}
-function refreshStatus(){
-    authFetch('/api/frpc/status').then(r=>r.json()).then(d=>{
-        let dot=document.getElementById('svc-status');
-        dot.className='status-dot '+(d.running?'online':'offline');
-        let tbody=document.getElementById('tunnel-list');
-        if(!d.tunnels||d.tunnels.length===0){
-            tbody.innerHTML='<tr><td colspan="5" style="color:#64748b">'+(d.error||'无隧道')+'</td></tr>';
-            return;
-        }
-        tbody.innerHTML=d.tunnels.map(t=>'<tr><td>'+t.name+'</td><td>'+t.type+'</td><td>'+t.local_addr+'</td><td>'+t.remote_addr+'</td><td><span class="status-dot '+(t.status==='online'?'online':'offline')+'"></span>'+t.status+'</td></tr>').join('');
-    }).catch(e=>console.error(e));
-}
-function loadConfig(){
-    authFetch('/api/frpc/config').then(r=>r.text()).then(t=>{
-        document.getElementById('config-editor').value=t;
-    });
-}
-function saveConfig(){
-    let content=document.getElementById('config-editor').value;
-    authFetch('/api/frpc/config',{method:'POST',body:content,headers:{'Content-Type':'text/plain'}})
-    .then(r=>r.json()).then(d=>{
-        showMsg('config-msg',d.message||d.error,d.success);
-    });
-}
-function saveAndRestart(){
-    saveConfig();
-    setTimeout(restartFrpc,500);
-}
-function restartFrpc(){
-    if(!confirm('确定要重启 frpc 服务吗?配置变更后重启会生效。')) return;
-    authFetch('/api/frpc/restart',{method:'POST'}).then(r=>r.json()).then(d=>{
-        showMsg('status-msg',d.message||d.error,d.success);
-        setTimeout(refreshStatus,2000);
-    });
-}
-function toggleSSH(){
-    let frame=document.getElementById('ssh-frame');
-    if(!sshConnected){
-        let proto=location.protocol==='https:'?'wss':'ws';
-        let host=location.hostname;
-        let port=location.port;
-        // ttyd runs on separate port, use external URL
-        frame.src='http://'+host+':40964';
-        frame.style.display='block';
-        sshConnected=true;
-    }else{
-        frame.style.display='none';
-        frame.src='';
-        sshConnected=false;
-    }
-}
-// Init
-refreshStatus();
-document.getElementById('ssh-external-link').href='http://'+location.hostname+':40964';
-</script>
-</body>
-</html>

+ 0 - 1185
assets/web/help.html

@@ -1,1185 +0,0 @@
-<!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;cursor:pointer;user-select:none}
-.card-title:hover{background:#37474f}
-.card-title .arrow{transition:transform 0.2s;font-size:12px}
-.card-title.collapsed .arrow{transform:rotate(-90deg)}
-.card-body{padding:16px 20px;line-height:1.8;font-size:14px}
-.card-body.hidden{display:none}
-h3{color:#1a1a2e;margin:16px 0 8px;font-size:15px;border-left:3px solid #667eea;padding-left:10px}
-h4{color:#333;margin:12px 0 6px;font-size:14px}
-table{width:100%;border-collapse:collapse;margin:8px 0 16px;font-size:13px}
-th{background:#f5f5f5;padding:8px 10px;text-align:left;font-weight:600;border-bottom:2px solid #e0e0e0;white-space:nowrap}
-td{padding:8px 10px;border-bottom:1px solid #f0f0f0;vertical-align:top}
-td:first-child{font-family:monospace;color:#d32f2f;white-space:nowrap;font-size:12px}
-code{background:#f5f5f5;padding:2px 6px;border-radius:3px;font-size:12px;color:#d32f2f;font-family:monospace}
-pre{background:#1e1e2e;color:#cdd6f4;padding:14px 18px;border-radius:6px;font-size:12px;overflow-x:auto;margin:8px 0 16px;line-height:1.6}
-pre code{background:none;color:#cdd6f4;padding:0;font-size:12px}
-.tip{background:#e8f5e9;border-left:4px solid #4caf50;padding:10px 14px;margin:8px 0;border-radius:0 4px 4px 0;font-size:13px}
-.warn{background:#fff3e0;border-left:4px solid #ff9800;padding:10px 14px;margin:8px 0;border-radius:0 4px 4px 0;font-size:13px}
-.note{background:#e3f2fd;border-left:4px solid #2196f3;padding:10px 14px;margin:8px 0;border-radius:0 4px 4px 0;font-size:13px}
-ul{padding-left:20px;margin:6px 0}
-li{margin:4px 0;font-size:13px;line-height:1.7}
-.toc{background:#fff;border-radius:8px;padding:16px 24px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,0.08)}
-.toc h2{font-size:16px;color:#1a1a2e;margin-bottom:10px;border-bottom:2px solid #667eea;padding-bottom:6px}
-.toc ol{padding-left:20px;columns:2;column-gap:30px}
-.toc li{margin:4px 0;font-size:13px;break-inside:avoid}
-.toc a{color:#667eea;text-decoration:none}
-.toc a:hover{text-decoration:underline}
-@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}
-.toc ol{columns:1}
-table{font-size:12px;display:block;overflow-x:auto}
-th,td{padding:6px 8px}
-td:first-child{font-size:11px}
-}
-@media(max-width:480px){
-.navbar .brand{font-size:13px}
-.navbar a{font-size:11px;padding:3px 6px}
-.card-body{padding:12px 14px}
-}
-</style>
-</head>
-<body>
-<nav class="navbar">
-  <span class="brand">🚛 车牌识别系统</span>
-  <a href="/">监控首页</a>
-  <a href="/video">视频预览</a>
-  <a href="/locks">锁定管理</a>
-  <a href="/help" class="active">帮助手册</a>
-</nav>
-
-<div class="container">
-
-<div class="toc">
-<h2>📋 目录</h2>
-<ol>
-  <li><a href="#sec1">系统概述</a></li>
-  <li><a href="#sec2">快速部署指南</a></li>
-  <li><a href="#sec3">config.ini 完整配置说明</a></li>
-  <li><a href="#sec4">RTSP 摄像头配置</a></li>
-  <li><a href="#sec5">车牌识别上传流程</a></li>
-  <li><a href="#sec6">MQTT 数据上报配置</a></li>
-  <li><a href="#sec7">称重系统对接</a></li>
-  <li><a href="#sec8">ROI 区域识别配置</a></li>
-  <li><a href="#sec9">交替锁定机制</a></li>
-  <li><a href="#sec10">登录认证与权限</a></li>
-  <li><a href="#sec11">frpc.toml 内网穿透配置详解</a></li>
-  <li><a href="#sec12">Nginx 反向代理配置详解</a></li>
-  <li><a href="#sec13">SSL/HTTPS 证书配置</a></li>
-  <li><a href="#sec14">系统监控与日志</a></li>
-  <li><a href="#sec15">Web 管理功能</a></li>
-  <li><a href="#sec16">数据库与备份</a></li>
-  <li><a href="#sec17">常见问题 FAQ</a></li>
-</ol>
-</div>
-
-<!-- 1. 系统概述 -->
-<div class="card" id="sec1">
-<div class="card-title" onclick="toggleCard(this)">1. 系统概述 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>系统简介</h3>
-<p>车牌识别系统(PlateRecApp)是一款基于 HyperLPR3 的实时车牌识别应用,运行在树莓派 5 (aarch64) 上,支持:</p>
-<ul>
-  <li>多路 RTSP 摄像头实时抓拍识别(进场/出场/侧面进/侧面出)</li>
-  <li>车牌识别结果自动上传至云端 API</li>
-  <li>称重系统对接(TCP 协议),自动关联车牌与称重数据</li>
-  <li>MQTT 协议实时推送识别结果</li>
-  <li>飞书群消息通知(异常报警)</li>
-  <li>Web 管理界面(监控首页、视频预览、系统配置、锁定管理等)</li>
-  <li>frpc 内网穿透管理 + Nginx stream + ssl_preread 协议分流(HTTP 自动跳 HTTPS)</li>
-  <li>登录认证与角色权限控制</li>
-  <li>SD 卡写入寿命保护(内存缓冲 + 每日定时写盘)</li>
-</ul>
-
-<h3>运行环境</h3>
-<table>
-  <tr><th>项目</th><th>要求</th></tr>
-  <tr><td>硬件</td><td>树莓派 5 8GB(推荐)</td></tr>
-  <tr><td>系统</td><td>Debian 13 (trixie) / Ubuntu 24.04 aarch64</td></tr>
-  <tr><td>依赖</td><td>HyperLPR3 SDK、FFmpeg、libcurl、OpenSSL、cJSON、Mosquitto、Nginx</td></tr>
-  <tr><td>内网穿透</td><td>frpc(通过 systemctl 管理)</td></tr>
-  <tr><td>协议分流</td><td>Nginx stream + ssl_preread(TCP 层 HTTP/HTTPS 自动检测分流)</td></tr>
-</table>
-
-<h3>网络架构总览(v2 — nginx stream + ssl_preread 方案)</h3>
-<pre>外网客户端                        frps 服务器                    本机(树莓派5)
-─────────                        ─────────                    ─────────────
-                                                               ┌─ TLS → nginx stream → C++ HTTPS:8080 (透传)
-https://mqtt.zhonjin.com:40963/  ─TCP─→  discover.zhonjin.com:9443  ─TCP─→  frpc ─→ nginx:40963 (stream+ssl_preread)
-http://mqtt.zhonjin.com:40963/   ─TCP─→            ↑                ─TCP─→  frpc ─→└─ HTTP → nginx:8081 (301→HTTPS)
-https://mqtt.zhonjin.com:40964/  ─TCP─→            ↑                ─TCP─→  frpc ─→ nginx:8082 (HTTPS代理) → ttyd:7681</pre>
-<div class="tip"><strong>v2 方案优势:</strong>公网只需一个端口 40963,nginx stream 在 TCP 层自动检测协议类型并分流。HTTP 自动跳 HTTPS,HTTPS 透传到 C++ 应用处理。用户只需记住一个地址。</div>
-
-<h3>目录结构</h3>
-<table>
-  <tr><th>路径</th><th>说明</th></tr>
-  <tr><td>PlateRecApp</td><td>主程序二进制</td></tr>
-  <tr><td>config.ini</td><td>主配置文件</td></tr>
-  <tr><td>config.txt</td><td>MD5 校验配置</td></tr>
-  <tr><td>hyperlpr3/</td><td>HyperLPR3 SDK 模型文件</td></tr>
-  <tr><td>assets/web/</td><td>Web 页面文件(HTML/CSS/JS)</td></tr>
-  <tr><td>data/</td><td>数据库目录(auth.db、upload_records.db、system_metrics.db)</td></tr>
-  <tr><td>photos/</td><td>抓拍图片存储目录</td></tr>
-  <tr><td>scripts/</td><td>运维脚本(sudoers 配置、密码重置等)</td></tr>
-</table>
-</div>
-</div>
-
-<!-- 2. 快速部署指南 -->
-<div class="card" id="sec2">
-<div class="card-title" onclick="toggleCard(this)">2. 快速部署指南 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>编译部署</h3>
-<ol>
-  <li>解压代码包:<code>tar xzf fix24_vXX.tar.gz</code></li>
-  <li>编译:<code>cd fix24/build && cmake .. && make -j4</code></li>
-  <li>部署文件到安装目录(如 <code>/opt/openAI/project/003.PlateRecAPP/</code>)</li>
-  <li>复制 HTML 文件:<code>cp -r assets/web/ /安装目录/assets/web/</code></li>
-  <li>配置 sudoers(frpc 管理需要):<code>sudo bash scripts/setup_frpc_sudoers.sh</code></li>
-  <li>安装 nginx stream 模块:<code>sudo apt install libnginx-mod-stream</code></li>
-  <li>部署 nginx stream 配置文件(详见第 12 章)</li>
-  <li>部署 SSL 证书(C++ 应用自管理,详见第 13 章)</li>
-  <li>修改 <code>config.ini</code> 配置(参考下文各节)</li>
-  <li>启动服务:<code>sudo systemctl restart nginx && sudo systemctl restart PlateRecApp</code></li>
-</ol>
-
-<h3>一键部署脚本(推荐)</h3>
-<p>代码包内置 <code>scripts/deploy.sh</code>,覆盖编译→备份→部署→服务安装→启动→健康检查全流程,默认部署目录 <code>/opt/openAI/project/003.PlateRecAPP</code>、运行用户 <code>stevenroc</code>、服务名 <code>PlateRecApp</code>(均可用环境变量 <code>APP_DIR</code>/<code>RUN_USER</code>/<code>SVC_NAME</code> 覆盖):</p>
-<pre># 首次:安装编译/运行依赖(需 root)
-sudo bash scripts/deploy.sh deps
-
-# 一键编译+部署+重启(部署前自动备份当前版本到 backup/,保留最近 5 个)
-sudo bash scripts/deploy.sh deploy      # 或直接 sudo bash scripts/deploy.sh
-
-# 日常运维
-bash scripts/deploy.sh status           # 服务状态 + 8080/8079 端口 + 进程 + 磁盘
-bash scripts/deploy.sh logs             # 跟踪 PlateRecApp.log
-bash scripts/deploy.sh journal          # 跟踪 systemd journal
-bash scripts/deploy.sh restart          # 重启服务
-sudo bash scripts/deploy.sh rollback    # 回滚到上一次备份
-sudo bash scripts/deploy.sh sudoers     # 配置 frpc 管理 sudoers 权限</pre>
-<div class="tip">脚本安全策略:<b>config.ini 已存在时永不覆盖</b>(首次部署才从模板生成);每次部署自动备份二进制+配置+Web资源+SDK;服务文件由脚本自动安装到 /etc/systemd/system/ 并设置开机自启。</div>
-
-<h3>systemd 服务配置</h3>
-<p>PlateRecApp 服务文件路径:<code>/etc/systemd/system/PlateRecApp.service</code>(由 deploy.sh 自动生成,内容等价于):</p>
-<pre>Unit]
-Description=PlateRecApp License Plate Recognition System
-After=network.target frpc.service
-
-[Service]
-User=stevenroc
-Group=stevenroc
-WorkingDirectory=/opt/openAI/project/003.PlateRecAPP
-ExecStartPre=/bin/sleep 10
-ExecStart=/opt/openAI/project/003.PlateRecAPP/PlateRecApp
-TimeoutStartSec=30
-TimeoutStopSec=10
-Restart=on-failure
-RestartSec=5
-StandardOutput=append:/opt/openAI/project/003.PlateRecAPP/PlateRecApp.log
-LimitNOFILE=1048576
-
-[Install]
-WantedBy=multi-user.target</pre>
-<div class="tip">PlateRecApp 以非 root 用户运行(stevenroc),frpc 管理通过 sudoers 授权。日志输出由 v36 内存缓冲系统接管,systemd 的 StandardOutput 仅作备份。</div>
-
-<h3>密码重置</h3>
-<p>如忘记管理员密码,运行:<code>python3 reset_passwords.py</code></p>
-
-<h3>Nginx 服务配置(v2 — stream 方案)</h3>
-<p>安装 stream 模块并部署配置:</p>
-<pre># 安装 stream 模块(必须)
-sudo apt install -y libnginx-mod-stream
-
-# 验证模块
-nginx -V 2>&amp;1 | grep stream
-
-# 替换 nginx.conf(详见第 12 章)
-sudo cp nginx-stream.conf /etc/nginx/nginx.conf
-
-# 验证配置
-sudo nginx -t
-
-# 启动/重载
-sudo systemctl enable nginx
-sudo systemctl restart nginx</pre>
-</div>
-</div>
-
-<!-- 3. config.ini 完整配置 -->
-<div class="card" id="sec3">
-<div class="card-title" onclick="toggleCard(this)">3. config.ini 完整配置说明 <span class="arrow">▼</span></div>
-<div class="card-body">
-<p>配置文件路径:<code>&lt;安装目录&gt;/config.ini</code>,ini 格式,section 和 key 均不区分大小写。</p>
-
-<h3>[config] 核心识别配置</h3>
-<table>
-  <tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>project_name</td><td>string</td><td>-</td><td>项目名称</td></tr>
-  <tr><td>point_number</td><td>string</td><td>-</td><td>站点编号</td></tr>
-  <tr><td>throughway</td><td>string</td><td>-</td><td>通道名称</td></tr>
-  <tr><td>rtsp_url_front_in</td><td>string</td><td>-</td><td>前侧进场摄像头 RTSP 地址</td></tr>
-  <tr><td>rtsp_url_front_out</td><td>string</td><td>-</td><td>前侧出场摄像头 RTSP 地址</td></tr>
-  <tr><td>rtsp_url_side_in</td><td>string</td><td>-</td><td>侧面进场摄像头 RTSP 地址</td></tr>
-  <tr><td>rtsp_url_side_out</td><td>string</td><td>-</td><td>侧面出场摄像头 RTSP 地址</td></tr>
-  <tr><td>app_api</td><td>string</td><td>-</td><td>云端上传 API 地址</td></tr>
-  <tr><td>app_key</td><td>string</td><td>-</td><td>API 认证 Key</td></tr>
-  <tr><td>app_secret</td><td>string</td><td>-</td><td>API 认证 Secret</td></tr>
-  <tr><td>DEBUG_LOG</td><td>int</td><td>0</td><td>调试日志开关(0=关闭,1=开启)</td></tr>
-  <tr><td>TestFlag</td><td>int</td><td>0</td><td>测试模式开关</td></tr>
-  <tr><td>wType</td><td>int</td><td>0</td><td>称重类型(0=禁用,1=启用)</td></tr>
-  <tr><td>TIME_WINDOW</td><td>int</td><td>5</td><td>识别时间窗口(分钟),同一车牌在窗口内不重复上传</td></tr>
-  <tr><td>MAX_PHOTO_GROUPS</td><td>int</td><td>100</td><td>最大照片组数缓存</td></tr>
-  <tr><td>in_out_interval</td><td>int</td><td>5</td><td>⚠️ 单位是<strong>分钟</strong>!交替锁定间隔(同一车牌进出最短时间),内部转为秒存储</td></tr>
-  <tr><td>PLATE_CONFIDENCE_THRESHOLD</td><td>float</td><td>0.7</td><td>车牌识别置信度阈值(0~1),低于此值不上传</td></tr>
-  <tr><td>PLATE_LOG_THRESHOLD</td><td>float</td><td>0.5</td><td>日志记录置信度阈值</td></tr>
-  <tr><td>AlternatingMerge</td><td>int</td><td>0</td><td>交替合并开关(0=关闭,1=开启)</td></tr>
-  <tr><td>hw_decode_mode</td><td>string</td><td>auto</td><td>硬件解码模式:auto/soft/drm</td></tr>
-  <tr><td>hw_decode_device</td><td>string</td><td>/dev/dri/renderD128</td><td>硬件解码设备路径</td></tr>
-  <tr><td>rtsp_transport</td><td>string</td><td>tcp</td><td>RTSP 传输协议:tcp/udp</td></tr>
-  <tr><td>PhotoMaxCapacityMB</td><td>int</td><td>1024</td><td>照片目录最大容量(MB),超限自动清理旧照片</td></tr>
-  <tr><td>LogRetentionDays</td><td>int</td><td>30</td><td>日志保留天数</td></tr>
-</table>
-
-<h3>[server] Web 服务配置</h3>
-<table>
-  <tr><th>参数</th><th>说明</th></tr>
-  <tr><td>ip</td><td>Web 服务监听地址(含端口),如 <code>0.0.0.0:8080</code>。v2 方案下 PlateRecApp 提供 HTTPS 服务(SSL 自管理),nginx stream 透传 TLS 流量到此处</td></tr>
-</table>
-
-<h3>[mqtt] MQTT 上报配置</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>MQTT_HOST</td><td>-</td><td>MQTT Broker 地址(填写后自动启用 MQTT)</td></tr>
-  <tr><td>MQTT_PORT</td><td>1883</td><td>MQTT Broker 端口</td></tr>
-  <tr><td>MQTT_USER</td><td>-</td><td>MQTT 用户名</td></tr>
-  <tr><td>MQTT_PASS</td><td>-</td><td>MQTT 密码</td></tr>
-  <tr><td>MQTT_TOPIC</td><td>-</td><td>推送主题</td></tr>
-  <tr><td>MQTT_CLIENT_ID</td><td>-</td><td>客户端 ID</td></tr>
-  <tr><td>PLATE_COLOR</td><td>-</td><td>车牌颜色标识(随 MQTT 消息上报)</td></tr>
-  <tr><td>VEHICLE_TYPE</td><td>-</td><td>车辆类型标识(随 MQTT 消息上报)</td></tr>
-</table>
-
-<h3>[auth] 登录认证配置</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>enabled</td><td>1</td><td>认证开关(0=关闭,1=开启)</td></tr>
-  <tr><td>db_path</td><td>data/auth.db</td><td>认证数据库路径</td></tr>
-  <tr><td>session_idle_timeout</td><td>1800</td><td>Session 空闲超时(秒)</td></tr>
-  <tr><td>session_max_timeout</td><td>28800</td><td>Session 最大超时(秒)</td></tr>
-  <tr><td>session_remember_timeout</td><td>604800</td><td>记住登录超时(秒)</td></tr>
-  <tr><td>max_failed_attempts</td><td>5</td><td>最大登录失败次数</td></tr>
-  <tr><td>lockout_duration</td><td>900</td><td>锁定时长(秒)</td></tr>
-</table>
-
-<h3>[ssl] HTTPS 配置(v2 — C++ 应用自管理)</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>enabled</td><td>1</td><td>HTTPS 开关。v2 方案下必须设为 1(C++ 应用直接处理 TLS)</td></tr>
-  <tr><td>cert_path</td><td>ssl/plate_fullchain.pem</td><td>SSL 证书路径(相对于安装目录)</td></tr>
-  <tr><td>key_path</td><td>ssl/plate_privkey.pem</td><td>SSL 私钥路径(相对于安装目录)</td></tr>
-</table>
-<div class="note">v2 方案变更:nginx stream 只做 TCP 分流,不解密 TLS。SSL 证书由 C++ 应用自己管理,<code>enabled</code> 必须为 1。</div>
-
-<h3>[sync] 车牌称重并行同步配置(v43 新增)</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>enabled</td><td>0</td><td>并行同步开关。<code>0</code>=串行v41模式(重量在照片上传成功后入队);<code>1</code>=并行v43模式(tb_num确定后立即并行触发重量读取+上传)</td></tr>
-  <tr><td>reporter_timeout_sec</td><td>30</td><td>重量上报API超时(秒),最小5</td></tr>
-  <tr><td>weight_retry_max</td><td>5</td><td>重量上传最大重试次数,最小0</td></tr>
-  <tr><td>weight_retry_interval_sec</td><td>10</td><td>重量上传重试间隔(秒),最小1</td></tr>
-  <tr><td>tb_num_dedup_sec</td><td>300</td><td>同联单编号去重窗口(秒),最小10。同一 tb_num 在此时间内只触发一次重量读取</td></tr>
-</table>
-<div class="tip"><b>v43 核心变更</b>:并行模式下,车牌识别成功并获取到 tb_num 后,照片上传和重量读取<strong>同时进行</strong>,互不阻塞。重量上报延迟从 v41 的 25~50s 降至 ≤5s,捕获率从 72% 提升至 ≥98%。</div>
-<div class="warn"><b>灰度策略</b>:首次部署建议保持 <code>enabled=0</code>(完全兼容v41行为),验证系统稳定后再改为 <code>enabled=1</code> 开启并行模式。修改 enabled 无需重启,秒级生效。</div>
-
-<h3>[system] 系统监控配置(v38 内存缓冲)</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>monitor_interval</td><td>2</td><td>监控采集间隔(秒),delta 法计算 CPU 使用率</td></tr>
-  <tr><td>temp_alert_threshold</td><td>70</td><td>温度告警阈值(°C)</td></tr>
-  <tr><td>cpu_alert_threshold</td><td>90</td><td>CPU 告警阈值(%)</td></tr>
-  <tr><td>db_path</td><td>data/system_metrics.db</td><td>监控数据库路径</td></tr>
-  <tr><td>history_retention_days</td><td>7</td><td>原始数据保留天数</td></tr>
-  <tr><td>aggregation_retention_days</td><td>365</td><td>聚合数据保留天数</td></tr>
-  <tr><td>flush_time</td><td>23:20</td><td>每日定时刷盘时间(HH:MM)</td></tr>
-</table>
-
-<h3>[frpc] 内网穿透配置</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>config_path</td><td>-</td><td>frpc.toml 配置文件路径</td></tr>
-  <tr><td>protected_tunnels</td><td>-</td><td>受保护隧道名称(逗号分隔)</td></tr>
-  <tr><td>auto_rollback</td><td>1</td><td>重启失败自动回滚(0/1)</td></tr>
-  <tr><td>admin_addr</td><td>127.0.0.1</td><td>frpc Admin API 地址</td></tr>
-  <tr><td>admin_port</td><td>7400</td><td>frpc Admin API 端口</td></tr>
-  <tr><td>admin_user</td><td>-</td><td>frpc Admin 用户名</td></tr>
-  <tr><td>admin_password</td><td>-</td><td>frpc Admin 密码</td></tr>
-</table>
-
-<h3>[log] 日志配置(v36 内存缓冲)</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>enabled</td><td>1</td><td>内存缓冲模式(1=内存缓冲,0=直写文件)</td></tr>
-  <tr><td>log_file</td><td>PlateRecApp.log</td><td>日志文件路径</td></tr>
-  <tr><td>buffer_size_mb</td><td>20</td><td>内存缓冲区上限(MB)</td></tr>
-  <tr><td>flush_time</td><td>23:20</td><td>每日定时刷盘时间(HH:MM)</td></tr>
-  <tr><td>retention_days</td><td>30</td><td>日志文件保留天数</td></tr>
-  <tr><td>redirect_stdout</td><td>1</td><td>捕获 stdout/stderr(0/1)</td></tr>
-  <tr><td>auto_flush_on_exit</td><td>1</td><td>退出时立即刷盘(0/1)</td></tr>
-</table>
-
-<h3>[feishu] 飞书通知配置</h3>
-<table>
-  <tr><th>参数</th><th>说明</th></tr>
-  <tr><td>APP_ID</td><td>飞书应用 App ID</td></tr>
-  <tr><td>APP_SECRET</td><td>飞书应用 App Secret</td></tr>
-  <tr><td>CHAT_ID</td><td>飞书群聊 ID</td></tr>
-  <tr><td>TIMEOUT_SECOND</td><td>请求超时时间(秒)</td></tr>
-  <tr><td>WarningSigns</td><td>抑制紧急报警(1=抑制,0=正常)</td></tr>
-</table>
-
-<h3>[weight] 称重系统配置</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>flagWeight</td><td>0</td><td>称重系统开关(0/1)</td></tr>
-  <tr><td>weight_server_ip</td><td>-</td><td>称重服务器 IP</td></tr>
-  <tr><td>weight_server_port</td><td>-</td><td>称重服务器端口</td></tr>
-  <tr><td>weight_threshold_in</td><td>0</td><td>进场称重阈值(吨)</td></tr>
-  <tr><td>weight_threshold_out</td><td>0</td><td>出场称重阈值(吨)</td></tr>
-  <tr><td>default_entry_weight</td><td>0</td><td>默认进场重量</td></tr>
-  <tr><td>default_exit_weight</td><td>0</td><td>默认出场重量</td></tr>
-  <tr><td>weight_detection_time</td><td>5000</td><td>称重检测时间(毫秒)</td></tr>
-  <tr><td>tcp_connect_timeout</td><td>3000</td><td>TCP 连接超时(毫秒)</td></tr>
-  <tr><td>MAX_WEIGHT</td><td>100</td><td>最大称重值(吨)</td></tr>
-  <tr><td>STABLE_SAMPLE_COUNT</td><td>5</td><td>稳定采样次数</td></tr>
-  <tr><td>STABLE_THRESHOLD_KG</td><td>50</td><td>稳定判定阈值(公斤)</td></tr>
-  <tr><td>CANDIDATE_DATA_COUNT</td><td>3</td><td>候选数据次数</td></tr>
-  <tr><td>MAX_UPLOAD_RETRIES</td><td>3</td><td>上传最大重试次数</td></tr>
-  <tr><td>RETRY_DELAY_MS</td><td>1000</td><td>重试延迟(毫秒)</td></tr>
-</table>
-
-<h3>[terminal] 终端/SSH 配置</h3>
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>ttyd_port</td><td>7681</td><td>ttyd Web 终端端口</td></tr>
-  <tr><td>ttyd_credential</td><td>-</td><td>ttyd 登录凭据(user:password)</td></tr>
-  <tr><td>ttyd_max_clients</td><td>5</td><td>ttyd 最大客户端数</td></tr>
-  <tr><td>ssh_keys_path</td><td>-</td><td>SSH 公钥存储路径</td></tr>
-</table>
-</div>
-</div>
-
-<!-- 4. RTSP 摄像头配置 -->
-<div class="card" id="sec4">
-<div class="card-title" onclick="toggleCard(this)">4. RTSP 摄像头配置 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>摄像头地址格式</h3>
-<p>标准 RTSP 地址格式:</p>
-<pre>rtsp://用户名:密码@摄像头IP:554/stream</pre>
-
-<h3>四路摄像头说明</h3>
-<table>
-  <tr><th>参数</th><th>方向</th><th>说明</th></tr>
-  <tr><td>rtsp_url_front_in</td><td>前→进</td><td>前方摄像头,捕捉进场车辆</td></tr>
-  <tr><td>rtsp_url_front_out</td><td>前→出</td><td>前方摄像头,捕捉出场车辆</td></tr>
-  <tr><td>rtsp_url_side_in</td><td>侧→进</td><td>侧面摄像头,捕捉进场车辆</td></tr>
-  <tr><td>rtsp_url_side_out</td><td>侧→出</td><td>侧面摄像头,捕捉出场车辆</td></tr>
-</table>
-
-<h3>硬件解码(树莓派5推荐配置)</h3>
-<table>
-  <tr><th>模式</th><th>值</th><th>说明</th></tr>
-  <tr><td>自动</td><td>auto</td><td>优先硬件解码,不可用则回退软件</td></tr>
-  <tr><td>软件解码</td><td>soft</td><td>CPU 解码,兼容性最好</td></tr>
-  <tr><td>DRM 硬件</td><td>drm</td><td>DRM 硬件加速(树莓派5推荐)</td></tr>
-</table>
-<div class="tip">树莓派 5 推荐:<code>hw_decode_mode=drm</code>,<code>hw_decode_device=/dev/dri/renderD128</code>,<code>rtsp_transport=tcp</code></div>
-</div>
-</div>
-
-<!-- 5. 车牌识别上传流程 -->
-<div class="card" id="sec5">
-<div class="card-title" onclick="toggleCard(this)">5. 车牌识别上传完整流程 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>识别流程</h3>
-<ol>
-  <li><strong>摄像头抓拍</strong>:RTSP 视频流实时解码,检测到车辆运动时触发抓拍</li>
-  <li><strong>车牌识别</strong>:HyperLPR3 引擎识别车牌号,返回车牌文本 + 置信度</li>
-  <li><strong>置信度过滤</strong>:低于 <code>PLATE_CONFIDENCE_THRESHOLD</code> 则丢弃</li>
-  <li><strong>时间窗口去重</strong>:同一车牌在 <code>TIME_WINDOW</code> 分钟内不重复上传</li>
-  <li><strong>交替锁定检查</strong>:同一车牌进出间隔低于 <code>in_out_interval</code> 分钟则锁定</li>
-  <li><strong>ROI 过滤</strong>:如启用 ROI,只识别指定区域内的车牌</li>
-  <li><strong>数据上传</strong>:HTTP POST 将识别结果 + 照片上传至 <code>app_api</code></li>
-  <li><strong>MQTT 推送</strong>:如启用 MQTT,同步推送识别结果</li>
-  <li><strong>飞书通知</strong>:异常报警发送飞书群消息</li>
-</ol>
-
-<h3>照片存储与自动清理</h3>
-<ul>
-  <li>照片保存在 <code>PlateJPG/</code> 目录下,文件名格式:<code>车牌号_时间戳_方向.jpg</code>(如 <code>沪FQ7108_1787365677_side_out.jpg</code>)</li>
-  <li>系统每日凌晨 <strong>2:00</strong> 自动执行照片清理任务(间隔 24 小时)</li>
-  <li>清理策略:当 <code>PlateJPG/</code> 目录下所有 .jpg 文件总大小超过 <code>PhotoMaxCapacityMB</code>(默认 1024MB)时,按文件修改时间从旧到新逐个删除,直到总大小降至阈值以下</li>
-  <li>删除照片后递归清理空子目录</li>
-  <li>清理日志输出示例:
-<pre>[照片清理] PlateJPG/ 当前 1156 MB / 1024 MB 阈值,共 8234 个文件
-[照片清理] 删除 1523 张照片(按时间从旧到新),释放 156 MB,当前 1000 MB
-[照片清理] 同步清空DB路径字段: 2847 个字段已更新</pre>
-  </li>
-</ul>
-
-<h4>照片清理与数据库同步(v41 修复)</h4>
-<p>照片被物理删除后,系统同步清空 <code>upload_records</code> 表中对应的 <code>lo_photo_path</code> / <code>hi_photo_path</code> 字段(设为 NULL):</p>
-<ul>
-  <li>业务记录(车牌号、联单编号、抓拍时间、上传状态等)<strong>完整保留</strong>,仅清空路径字段</li>
-  <li>Web 前端路径为 NULL 时显示灰色斜体「已清理」占位,不再显示 broken image</li>
-  <li>即使 DB 清理与页面加载存在竞态,<code>&lt;img onerror&gt;</code> 也会自动替换为「已清理」</li>
-  <li>程序启动时自动执行一次孤儿路径扫描(<code>db_purge_missing_photo_paths</code>),修复历史遗留数据</li>
-</ul>
-<div class="tip">💡 调整容量阈值:修改 <code>config.ini</code> 中 <code>PhotoMaxCapacityMB</code> 的值并重启服务。例如设为 2048 表示允许 2GB 照片存储。</div>
-</div>
-</div>
-
-<!-- 6. MQTT -->
-<div class="card" id="sec6">
-<div class="card-title" onclick="toggleCard(this)">6. MQTT 数据上报配置 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>启用条件</h3>
-<p>在 <code>[mqtt]</code> 段中填写 <code>MQTT_HOST</code> 即自动启用 MQTT 推送。</p>
-
-<h3>配置示例</h3>
-<pre>[mqtt]
-MQTT_HOST=mqtt.example.com
-MQTT_PORT=1883
-MQTT_USER=username
-MQTT_PASS=password
-MQTT_TOPIC=plate/recognition
-MQTT_CLIENT_ID=plate_rec_station_01</pre>
-<div class="warn">MQTT 连接失败时系统会自动重连,不影响识别和上传功能。</div>
-</div>
-</div>
-
-<!-- 7. 称重系统 -->
-<div class="card" id="sec7">
-<div class="card-title" onclick="toggleCard(this)">7. 称重系统对接 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>工作原理</h3>
-<p>称重系统通过 TCP 协议与称重地磅通信,车牌识别成功后自动读取称重数据并关联上传。</p>
-
-<h3>重量判定逻辑</h3>
-<ul>
-  <li>连续 <code>STABLE_SAMPLE_COUNT</code> 次读数变化 &lt; <code>STABLE_THRESHOLD_KG</code> 公斤,视为稳定</li>
-  <li>稳定后取 <code>CANDIDATE_DATA_COUNT</code> 次读数的中值作为最终重量</li>
-  <li>重量超过 <code>MAX_WEIGHT</code> 吨视为异常,丢弃</li>
-</ul>
-
-<h3>阈值说明</h3>
-<ul>
-  <li><code>weight_threshold_in</code>:进场最低重量(吨),低于此值可能表示空车</li>
-  <li><code>weight_threshold_out</code>:出场最低重量(吨)</li>
-  <li><code>default_entry_weight</code>:无法获取称重数据时的默认进场重量</li>
-  <li><code>default_exit_weight</code>:无法获取称重数据时的默认出场重量</li>
-  <li><code>weight_detection_time</code>:称重检测时间(毫秒,默认 5000)</li>
-  <li><code>tcp_connect_timeout</code>:TCP 连接超时(毫秒,默认 3000)</li>
-  <li><code>MAX_UPLOAD_RETRIES</code>:上传最大重试次数(默认 3)</li>
-  <li><code>RETRY_DELAY_MS</code>:重试延迟(毫秒,默认 1000)</li>
-</ul>
-</div>
-</div>
-
-<!-- 8. ROI -->
-<div class="card" id="sec8">
-<div class="card-title" onclick="toggleCard(this)">8. ROI 区域识别配置 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>配置方式</h3>
-<ul>
-  <li><strong>Web 可视化</strong>:首页连接状态面板中拖拽设置 ROI 区域</li>
-  <li><strong>手动配置</strong>:在 config.ini 的 <code>[config]</code> 段填写坐标值</li>
-</ul>
-<h3>参数说明</h3>
-<table>
-  <tr><th>参数</th><th>说明</th></tr>
-  <tr><td>roi_in_x/y/w/h</td><td>进场 ROI 区域坐标和尺寸(像素)</td></tr>
-  <tr><td>roi_in_enabled</td><td>1=启用进场 ROI</td></tr>
-  <tr><td>roi_out_x/y/w/h</td><td>出场 ROI 区域坐标和尺寸</td></tr>
-  <tr><td>roi_out_enabled</td><td>1=启用出场 ROI</td></tr>
-  <tr><td>roi_debug_enabled</td><td>1=在抓拍图上绘制 ROI 框(调试用)</td></tr>
-</table>
-</div>
-</div>
-
-<!-- 9. 交替锁定 -->
-<div class="card" id="sec9">
-<div class="card-title" onclick="toggleCard(this)">9. 交替锁定机制 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>机制说明</h3>
-<p>防止同一车牌短时间内进出频繁触发(如车辆在门口调头)。同一车牌从"进"切换到"出"必须间隔 <code>in_out_interval</code> 分钟以上。</p>
-
-<h3>工作流程</h3>
-<ol>
-  <li>车牌 A 被识别为"进"方向</li>
-  <li>5 分钟内(默认)车牌 A 再次被识别为"出"方向</li>
-  <li>系统判定为异常,创建交替锁定记录</li>
-  <li>锁定期间该车牌不会被上传</li>
-  <li>超过 2 小时锁定自动清零,或手动在"锁定管理"页面清除</li>
-</ol>
-
-<table>
-  <tr><th>参数</th><th>默认值</th><th>说明</th></tr>
-  <tr><td>in_out_interval</td><td>5</td><td>交替锁定间隔(⚠️ 单位分钟),小于此间隔的进/出切换会被锁定</td></tr>
-  <tr><td>AlternatingMerge</td><td>0</td><td>交替合并开关</td></tr>
-</table>
-<div class="warn"><code>in_out_interval</code> 单位是<strong>分钟</strong>,不是秒!填 5 表示 5 分钟 = 300 秒。</div>
-</div>
-</div>
-
-<!-- 10. 认证与权限 -->
-<div class="card" id="sec10">
-<div class="card-title" onclick="toggleCard(this)">10. 登录认证与权限 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>角色定义</h3>
-<table>
-  <tr><th>角色</th><th>Role值</th><th>可访问页面</th></tr>
-  <tr><td>普通用户</td><td>0</td><td>首页、视频、称重、锁定管理</td></tr>
-  <tr><td>管理员</td><td>1</td><td>普通用户 + 系统配置</td></tr>
-  <tr><td>超级管理员</td><td>2</td><td>所有页面(含系统监控、frpc管理、SSH密钥、日志、帮助)</td></tr>
-</table>
-
-<h3>默认账号</h3>
-<table>
-  <tr><th>用户名</th><th>密码</th><th>角色</th></tr>
-  <tr><td>admin</td><td>zhongjin188A</td><td>超级管理员</td></tr>
-  <tr><td>zhonjin</td><td>zhonjin</td><td>普通用户</td></tr>
-</table>
-
-<h3>安全特性</h3>
-<ul>
-  <li>密码 bcrypt 加密存储(cost=12)</li>
-  <li>连续登录失败锁定账号</li>
-  <li>CSRF Token 防护</li>
-  <li>Session 超时自动失效</li>
-  <li>登录速率限制</li>
-</ul>
-</div>
-</div>
-
-<!-- 11. frpc.toml 详解 -->
-<div class="card" id="sec11">
-<div class="card-title" onclick="toggleCard(this)">11. frpc.toml 内网穿透配置详解 <span class="arrow">▼</span></div>
-<div class="card-body">
-
-<h3>配置文件位置</h3>
-<p>frpc 配置文件路径由 <code>config.ini</code> 的 <code>[frpc] config_path</code> 指定,通常为安装目录下的 <code>frpc.toml</code>。</p>
-
-<h3>v2 方案 — nginx stream 分流(推荐)</h3>
-<p>使用 nginx stream + ssl_preread 方案,<strong>只需一个公网端口 40963</strong> 即可同时处理 HTTP 和 HTTPS:</p>
-<pre># frpc.toml — v2 方案(nginx stream 分流)
-
-serverAddr = "discover.zhonjin.com"
-serverPort = 9443
-auth.method = "token"
-auth.token  = "你的frps认证Token"
-transport.tls.enable = true
-clientID = "ssd_40960"
-
-# frpc Admin HTTP API
-webServer.addr = "127.0.0.1"
-webServer.port = 7400
-webServer.user = "admin"
-webServer.password = "你的管理密码"
-
-# 隧道1:主服务(外网40963→本机nginx:40963,stream自动分流HTTP/HTTPS)
-[[proxies]]
-name       = "plate_web"
-type       = "tcp"
-localIP    = "127.0.0.1"
-localPort  = 40963    # ← 转发到 nginx stream,不是 C++ 的 8080
-remotePort = 40963
-
-# 隧道2:ttyd终端(外网40964→本机nginx:8082→ttyd:7681)
-[[proxies]]
-name       = "ttyd_web"
-type       = "tcp"
-localIP    = "127.0.0.1"
-localPort  = 8082     # ← nginx HTTPS 反向代理(SSL 终止 + WebSocket)
-remotePort = 40964</pre>
-
-<h3>隧道说明(v2)</h3>
-<table>
-  <tr><th>隧道名</th><th>外网端口</th><th>本机端口</th><th>用途</th></tr>
-  <tr><td>plate_web</td><td>40963</td><td>nginx stream:40963</td><td>HTTP/HTTPS 合一,stream 自动分流</td></tr>
-  <tr><td>ttyd_web</td><td>40964</td><td>nginx HTTPS:8082</td><td>Web 终端(nginx SSL→ttyd:7681)</td></tr>
-</table>
-
-<h3>v2 数据流向</h3>
-<pre>HTTP  流量: 公网:40963 → frpc → nginx:40963 (stream) → $ssl_preread=""  → 127.0.0.1:8081 (nginx HTTP) → 301 重定向到 HTTPS
-HTTPS 流量: 公网:40963 → frpc → nginx:40963 (stream) → $ssl_preread≠"" → 127.0.0.1:8080 (C++ HTTPS) → 直接处理
-Web SSH:  公网:40964 → frpc → nginx:8082 (HTTPS反向代理, SSL终止) → 127.0.0.1:7681 (ttyd HTTP)</pre>
-
-<div class="tip"><strong>v1→v2 迁移注意:</strong>不再需要 40965 端口。HTTPS 主服务统一使用 <code>https://mqtt.zhonjin.com:40963/</code>。</div>
-
-<h3>参数详解</h3>
-<table>
-  <tr><th>参数</th><th>说明</th></tr>
-  <tr><td>serverAddr</td><td>frps 服务器域名或 IP</td></tr>
-  <tr><td>serverPort</td><td>frps 服务端口(默认 7000,本项目用 9443)</td></tr>
-  <tr><td>auth.method</td><td>认证方式,固定为 <code>token</code></td></tr>
-  <tr><td>auth.token</td><td>与 frps 约定的认证 Token</td></tr>
-  <tr><td>webServer.addr/port</td><td>frpc Admin API 监听地址,PlateRecApp 通过此接口获取隧道状态</td></tr>
-  <tr><td>webServer.user/password</td><td>Admin API 认证凭据(需与 config.ini [frpc] 段一致)</td></tr>
-  <tr><td>[[proxies]] name</td><td>隧道唯一名称,PlateRecApp Web 界面按名称识别</td></tr>
-  <tr><td>[[proxies]] type</td><td>隧道类型,本项目全部使用 <code>tcp</code>(透传 TCP 流)</td></tr>
-  <tr><td>[[proxies]] localPort</td><td>本机目标端口,frpc 将外网流量转发到此端口</td></tr>
-  <tr><td>[[proxies]] remotePort</td><td>frps 服务器暴露的外网端口,客户端通过此端口访问</td></tr>
-</table>
-
-<h3>隧道保护</h3>
-<p>在 <code>config.ini [frpc]</code> 段配置受保护隧道,防止 Web 界面误删关键隧道:</p>
-<pre>protected_tunnels = plate_web,ttyd_web</pre>
-<div class="warn">受保护的隧道无法通过 Web 管理界面删除。如需删除,必须先修改 config.ini 中的 <code>protected_tunnels</code> 字段。</div>
-
-<h3>frpc 管理命令</h3>
-<pre># 查看 frpc 状态
-systemctl status frpc
-
-# 重启 frpc
-sudo systemctl restart frpc
-
-# 查看 frpc 日志
-tail -f /var/log/frpc.log
-
-# 通过 Admin API 查看隧道状态
-curl -u admin:密码 http://127.0.0.1:7400/api/status/tcp</pre>
-</div>
-</div>
-
-<!-- 12. Nginx 反向代理详解 -->
-<div class="card" id="sec12">
-<div class="card-title" onclick="toggleCard(this)">12. Nginx stream + ssl_preread 配置详解(v2) <span class="arrow">▼</span></div>
-<div class="card-body">
-
-<h3>架构说明</h3>
-<p>v2 方案使用 nginx 的 <code>stream</code> 模块 + <code>ssl_preread</code> 功能,在 <strong>TCP 层面</strong>检测流量类型并自动分流:</p>
-<ul>
-  <li><strong>端口 40963</strong>:nginx stream 监听,检测协议类型(TLS / HTTP)</li>
-  <li><strong>TLS 流量</strong> → 透传到 C++ HTTPS 服务(127.0.0.1:8080)</li>
-  <li><strong>HTTP 流量</strong> → nginx HTTP 模块(127.0.0.1:8081)→ 301 重定向到 HTTPS</li>
-</ul>
-<div class="tip"><strong>关键区别:</strong>stream 模块是 TCP 层代理,不解密 TLS 流量。SSL 证书由 C++ 应用自己管理,nginx 只做协议检测和分流。</div>
-
-<h3>nginx.conf 完整配置</h3>
-<p>文件路径:<code>/etc/nginx/nginx.conf</code>(替换整个文件)</p>
-<pre># 加载 stream 动态模块
-load_module modules/ngx_stream_module.so;
-
-events {
-    worker_connections 1024;
-}
-
-# ============================================================
-# stream 块 — TCP 层协议分流(在 http 块外面)
-# ============================================================
-stream {
-    # 根据 ssl_preread 检测结果选择后端
-    # $ssl_preread_protocol 的可能值:
-    #   ""          → HTTP 流量(非 TLS)
-    #   "TLSv1.2"   → TLS 1.2
-    #   "TLSv1.3"   → TLS 1.3
-    map $ssl_preread_protocol $backend {
-        ""        http_backend;    # HTTP → nginx HTTP 8081
-        default   tls_backend;     # TLS → C++ HTTPS 8080
-    }
-
-    # TLS 后端 — C++ HTTPS 服务
-    upstream tls_backend {
-        server 127.0.0.1:8080;
-    }
-
-    # HTTP 后端 — nginx HTTP 301 重定向
-    upstream http_backend {
-        server 127.0.0.1:8081;
-    }
-
-    # 主服务器 — 监听公网端口
-    server {
-        listen 40963;
-        proxy_pass $backend;
-        ssl_preread on;          # 启用 SSL 预读检测协议
-    }
-}
-
-# ============================================================
-# http 块 — HTTP → HTTPS 301 重定向
-# ============================================================
-http {
-    sendfile on;
-    tcp_nopush on;
-    tcp_nodelay on;
-    keepalive_timeout 65;
-
-    include /etc/nginx/mime.types;
-    default_type application/octet-stream;
-
-    access_log /var/log/nginx/access.log;
-    error_log  /var/log/nginx/error.log;
-
-    # HTTP → HTTPS 301 重定向
-    server {
-        listen 8081;
-        server_name _;
-        return 301 https://$host:40963$request_uri;
-    }
-
-    # ============================================================
-    # ttyd Web 终端 HTTPS 反向代理(端口 8082)
-    # ============================================================
-    # frpc 隧道:外网 40964 → nginx:8082 → ttyd:7681
-    # nginx 做 SSL 终止,再通过 HTTP 代理到 ttyd(含 WebSocket 支持)
-    server {
-        listen 8082 ssl;
-        server_name _;
-
-        ssl_certificate     /etc/nginx/ssl/plate_fullchain.pem;
-        ssl_certificate_key /etc/nginx/ssl/plate_privkey.pem;
-        ssl_protocols TLSv1.2 TLSv1.3;
-        ssl_ciphers HIGH:!aNULL:!MD5:!RC4;
-        ssl_prefer_server_ciphers on;
-        ssl_session_cache shared:SSL_Ttyd:5m;
-        ssl_session_timeout 1h;
-
-        location / {
-            proxy_pass http://127.0.0.1:7681;
-            proxy_set_header Host $host;
-            proxy_set_header X-Real-IP $remote_addr;
-            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-            proxy_set_header X-Forwarded-Proto $scheme;
-
-            # WebSocket 支持(ttyd 核心通信方式)
-            proxy_http_version 1.1;
-            proxy_set_header Upgrade $http_upgrade;
-            proxy_set_header Connection "upgrade";
-
-            # ttyd 长连接超时
-            proxy_read_timeout 86400s;
-            proxy_send_timeout 86400s;
-        }
-    }
-}</pre>
-
-<h3>核心原理</h3>
-<table>
-  <tr><th>组件</th><th>作用</th><th>说明</th></tr>
-  <tr><td>stream 块</td><td>TCP 层代理</td><td>与 http 块平级,不处理 HTTP 协议,只做 TCP 转发</td></tr>
-  <tr><td>ssl_preread on</td><td>SSL 预读</td><td>在 TLS 握手阶段读取 ClientHello,获取协议版本,不解密</td></tr>
-  <tr><td>map $ssl_preread_protocol</td><td>协议分流</td><td>空字符串 = HTTP,非空 = TLS</td></tr>
-  <tr><td>upstream tls_backend</td><td>TLS 后端</td><td>透传到 C++ 应用(8080),C++ 自己处理 TLS 握手</td></tr>
-  <tr><td>upstream http_backend</td><td>HTTP 后端</td><td>转发到 nginx HTTP 8081,返回 301 重定向</td></tr>
-  <tr><td>server 8082 ssl</td><td>ttyd HTTPS 代理</td><td>SSL 终止后代理到 ttyd:7681,支持 WebSocket</td></tr>
-</table>
-
-<h3>部署步骤</h3>
-<ol>
-  <li>安装 stream 模块:<code>sudo apt install -y libnginx-mod-stream</code></li>
-  <li>验证模块:<code>ls -l /usr/lib/nginx/modules/ngx_stream_module.so</code></li>
-  <li>备份原配置:<code>sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak</code></li>
-  <li>替换配置:<code>sudo cp nginx-stream.conf /etc/nginx/nginx.conf</code></li>
-  <li>删除默认站点(如有):<code>sudo rm -f /etc/nginx/sites-enabled/default</code></li>
-  <li>验证配置:<code>sudo nginx -t</code></li>
-  <li>启动 nginx:<code>sudo systemctl enable nginx && sudo systemctl restart nginx</code></li>
-</ol>
-
-<h3>前置条件</h3>
-<table>
-  <tr><th>项目</th><th>要求</th></tr>
-  <tr><td>nginx 版本</td><td>≥ 1.11.5(支持 stream 模块)</td></tr>
-  <tr><td>stream 模块</td><td>libnginx-mod-stream(Debian)或 nginx-mod-stream(CentOS)</td></tr>
-  <tr><td>C++ 应用</td><td>必须启用 HTTPS,监听 127.0.0.1:8080</td></tr>
-  <tr><td>SSL 证书</td><td>C++ 应用自行管理(config.ini 的 [ssl] 段)</td></tr>
-</table>
-
-<h3>验证方法</h3>
-<pre># 检查端口监听
-ss -tlnp | grep -E '40963|8080|8081'
-# 预期输出:
-# LISTEN  0.0.0.0:40963  nginx (stream)
-# LISTEN  127.0.0.1:8081  nginx (http 301)
-# LISTEN  0.0.0.0:8080  PlateRecApp (HTTPS)
-
-# 测试 HTTP 重定向(应返回 301)
-curl -I http://localhost:40963/
-# 预期:HTTP/1.1 301 Moved Permanently
-#       Location: https://localhost:40963/
-
-# 测试 HTTPS(应返回 200)
-curl -kI https://localhost:40963/
-# 预期:HTTP/1.1 200 OK
-
-# 浏览器测试(清除缓存或用无痕模式)
-# 访问 http://mqtt.zhonjin.com:40963/ → 自动跳转到 https://mqtt.zhonjin.com:40963/</pre>
-
-<h3>端口规划总结</h3>
-<table>
-  <tr><th>端口</th><th>服务</th><th>说明</th></tr>
-  <tr><td><strong>40963</strong></td><td>nginx stream</td><td>公网入口,TLS/HTTP 分流</td></tr>
-  <tr><td>8081</td><td>nginx http</td><td>HTTP 301 重定向(仅内部)</td></tr>
-  <tr><td>8080</td><td>C++ HTTPS</td><td>PlateRecApp Web 服务(SSL 自管理)</td></tr>
-  <tr><td><strong>40964</strong></td><td>nginx HTTPS→ttyd</td><td>Web 终端(nginx:8082 SSL终止→ttyd:7681)</td></tr>
-  <tr><td>8082</td><td>nginx HTTPS 代理</td><td>ttyd Web 终端 HTTPS 反向代理(仅内部)</td></tr>
-  <tr><td>7400</td><td>frpc Admin</td><td>隧道状态查询(仅 127.0.0.1)</td></tr>
-</table>
-</div>
-</div>
-
-<!-- 13. SSL/HTTPS 证书配置 -->
-<div class="card" id="sec13">
-<div class="card-title" onclick="toggleCard(this)">13. SSL/HTTPS 证书配置(v2 — C++ 应用自管理) <span class="arrow">▼</span></div>
-<div class="card-body">
-
-<div class="note"><strong>v2 方案变更:</strong>nginx stream 只做 TCP 层协议分流,不解密 TLS 流量。SSL 证书由 C++ 应用(PlateRecApp)自己管理,通过 config.ini 的 <code>[ssl]</code> 段配置。nginx 不再需要 SSL 证书。</div>
-
-<h3>config.ini [ssl] 段配置</h3>
-<pre>[ssl]
-enabled=1
-cert_path=ssl/plate_fullchain.pem
-key_path=ssl/plate_privkey.pem</pre>
-<div class="tip">v2 方案下 SSL 证书文件放在安装目录的 <code>ssl/</code> 子目录下(或任意自定义路径),不再需要 <code>/etc/nginx/ssl/</code>。</div>
-
-<h3>方式一:Let's Encrypt 证书 — 阿里云 DNS 验证(推荐)</h3>
-<p>本项目使用 frpc 内网穿透,<strong>无公网 80 端口</strong>,因此不能使用传统的 HTTP-01 验证。推荐使用 <strong>DNS-01 验证</strong>,通过阿里云 DNS API 自动完成,<strong>不需要任何端口</strong>。</p>
-
-<h4>方案对比</h4>
-<table>
-  <tr><th>验证方式</th><th>端口要求</th><th>通配符</th><th>自动续期</th><th>本项目适用</th></tr>
-  <tr><td>HTTP-01 (standalone)</td><td>公网 80</td><td>❌</td><td>✅</td><td>❌ 80 端口不可用</td></tr>
-  <tr><td>TLS-ALPN-01</td><td>公网 443</td><td>❌</td><td>✅</td><td>❌ 443 端口不可用</td></tr>
-  <tr><td><strong>DNS-01 (dns-aliyun)</strong></td><td><strong>无</strong></td><td><strong>✅</strong></td><td><strong>✅</strong></td><td><strong>✅ 推荐</strong></td></tr>
-  <tr><td>手动 DNS</td><td>无</td><td>✅</td><td>❌ 手动</td><td>⚠️ 每 90 天手动操作</td></tr>
-</table>
-
-<h4>一键部署</h4>
-<pre># 1. 安装 certbot + 阿里云 DNS 插件
-sudo apt update
-sudo apt install -y certbot python3-pip
-sudo pip3 install certbot-dns-aliyun
-
-# 2. 配置阿里云 AccessKey(RAM 子账号,需 AliyunDNSFullAccess 权限)
-sudo mkdir -p /etc/letsencrypt
-sudo vim /etc/letsencrypt/aliyun.ini
-# 内容:
-#   dns_aliyun_access_key = 你的AccessKeyID
-#   dns_aliyun_access_key_secret = 你的AccessKeySecret
-sudo chmod 600 /etc/letsencrypt/aliyun.ini
-
-# 3. 一键申请通配符证书(自动通过 DNS API 验证)
-sudo certbot certonly \
-    --authenticator dns-aliyun \
-    --dns-aliyun-credentials /etc/letsencrypt/aliyun.ini \
-    --dns-aliyun-propagation-seconds 30 \
-    --server https://acme-v02.api.letsencrypt.org/directory \
-    --agree-tos --no-eff-email \
-    --email admin@zhonjin.com \
-    -d mqtt.zhonjin.com \
-    -d "*.zhonjin.com"
-
-# 4. 部署证书到 PlateRecApp
-mkdir -p ssl/
-sudo cp /etc/letsencrypt/live/mqtt.zhonjin.com/fullchain.pem ssl/plate_fullchain.pem
-sudo cp /etc/letsencrypt/live/mqtt.zhonjin.com/privkey.pem   ssl/plate_privkey.pem
-sudo chown stevenroc:stevenroc ssl/*.pem
-chmod 644 ssl/plate_fullchain.pem
-chmod 600 ssl/plate_privkey.pem
-
-# 5. 配置自动续期(每天 3:00 检查,续期后自动部署)
-(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --deploy-hook /opt/openAI/project/003.PlateRecAPP/scripts/renew_ssl.sh") | crontab -
-
-# 6. 重启 PlateRecApp
-sudo systemctl restart PlateRecApp</pre>
-
-<h4>续期部署脚本 renew_ssl.sh</h4>
-<pre>#!/bin/bash
-# /opt/openAI/project/003.PlateRecAPP/scripts/renew_ssl.sh
-DOMAIN="mqtt.zhonjin.com"
-SSL_DIR="/opt/openAI/project/003.PlateRecAPP/ssl"
-
-cp /etc/letsencrypt/live/${DOMAIN}/fullchain.pem ${SSL_DIR}/plate_fullchain.pem
-cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem   ${SSL_DIR}/plate_privkey.pem
-chown stevenroc:stevenroc ${SSL_DIR}/*.pem
-chmod 644 ${SSL_DIR}/plate_fullchain.pem
-chmod 600 ${SSL_DIR}/plate_privkey.pem
-systemctl restart PlateRecApp
-echo "$(date) SSL 证书续期部署成功" >> /opt/openAI/project/003.PlateRecAPP/ssl_renew.log</pre>
-
-<h4>阿里云 RAM 权限配置</h4>
-<ol>
-  <li>登录 <a href="https://ram.console.aliyun.com/" target="_blank">RAM 控制台</a> → 用户 → 创建用户</li>
-  <li>勾选 <strong>OpenAPI 调用访问</strong>,保存 AccessKey ID 和 Secret</li>
-  <li>添加权限:<code>AliyunDNSFullAccess</code>(云解析 DNS 管理)</li>
-</ol>
-<div class="tip">DNS 验证原理:certbot 通过阿里云 API 自动添加 <code>_acme-challenge.mqtt.zhonjin.com</code> TXT 记录,Let's Encrypt CA 查询 DNS 验证域名所有权,全程无需开放任何端口。</div>
-<div class="warn"><strong>前提:</strong>域名 mqtt.zhonjin.com 的 DNS 解析必须使用阿里云(<code>dns9.hichina.com</code>)。如果 DNS 在其他服务商,需先迁移到阿里云,或改用对应插件(如 <code>certbot-dns-cloudflare</code>、<code>certbot-dns-dnspod</code>)。</div>
-
-<h3>方式二:自签名证书</h3>
-<p>无公网域名时使用自签名证书,浏览器会显示警告但加密通信正常:</p>
-<pre># 创建证书目录
-mkdir -p ssl/
-
-# 生成自签名证书(10年有效期)
-openssl req -x509 -newkey rsa:2048 -nodes \
-  -keyout ssl/plate_privkey.pem \
-  -out    ssl/plate_fullchain.pem \
-  -days   3650 \
-  -subj   "/CN=mqtt.zhonjin.com/O=PlateRecApp/C=CN"
-
-# 设置权限
-chmod 600 ssl/plate_privkey.pem
-chmod 644 ssl/plate_fullchain.pem
-
-# 重启 PlateRecApp 加载证书
-sudo systemctl restart PlateRecApp</pre>
-
-<h3>证书文件说明</h3>
-<table>
-  <tr><th>文件</th><th>用途</th><th>config.ini 参数</th></tr>
-  <tr><td>plate_fullchain.pem</td><td>完整证书链(含中间 CA)</td><td>cert_path</td></tr>
-  <tr><td>plate_privkey.pem</td><td>私钥文件(严禁泄露)</td><td>key_path</td></tr>
-</table>
-
-<h3>v1→v2 迁移说明</h3>
-<div class="warn"><strong>v1 方案:</strong>SSL 证书由 nginx 管理(<code>/etc/nginx/ssl/</code>),nginx 做 HTTPS 终止后转发 HTTP 到 C++。<br>
-<strong>v2 方案:</strong>SSL 证书由 C++ 应用管理,nginx stream 只做 TCP 分流,不解密 TLS。不再需要 <code>/etc/nginx/ssl/</code> 目录。</div>
-</div>
-</div>
-
-<!-- 14. 系统监控与日志 -->
-<div class="card" id="sec14">
-<div class="card-title" onclick="toggleCard(this)">14. 系统监控与日志 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>系统监控(实时)</h3>
-<ul>
-  <li>CPU 使用率(delta 法计算,含每核使用率、负载均值)</li>
-  <li>内存使用率(总量/已用/可用)</li>
-  <li>磁盘使用率(根分区容量/已用/可用)</li>
-  <li>CPU 温度监控,超阈值告警(阈值由 <code>temp_alert_threshold</code> 配置)</li>
-  <li>CPU 使用率告警(阈值由 <code>cpu_alert_threshold</code> 配置)</li>
-  <li>PlateRecApp 进程资源占用(RSS 内存、CPU 占用)</li>
-  <li>frpc 运行状态、MQTT 连接状态、称重系统连接状态</li>
-</ul>
-<p>数据来源:通过读取 <code>/proc</code>、<code>/sys</code>、<code>statvfs</code> 等系统接口实时采集,页面每 2 秒自动刷新。</p>
-
-<h3>历史监控数据(v37+)</h3>
-<p>系统自动采集并存储监控数据到 SQLite 数据库(<code>data/system_metrics.db</code>),支持历史趋势查询:</p>
-<ul>
-  <li><strong>采集间隔</strong>:<code>monitor_interval</code>(默认 2 秒),delta 法计算 CPU 使用率,不阻塞采集线程</li>
-  <li><strong>定时刷盘</strong>:每日 <code>flush_time</code>(默认 23:20)将内存监控数据批量写入 <code>system_metrics.db</code>,减少 SD 卡写入次数(v38 起)</li>
-  <li><strong>三级存储</strong>:原始数据(2秒间隔)→ 小时聚合 → 日聚合,自动降采样</li>
-  <li><strong>自动清理</strong>:原始数据保留 <code>history_retention_days</code> 天(默认 7 天),聚合数据保留 <code>aggregation_retention_days</code> 天(默认 365 天)</li>
-  <li><strong>历史查询 API</strong>:<code>/api/monitor/history?range=1h|6h|24h|7d|30d</code>,自动选择合适的数据粒度</li>
-  <li><strong>统计接口</strong>:<code>/api/monitor/stats</code> 返回数据库记录数和运行状态</li>
-</ul>
-
-<h3>SD 卡写入保护策略(v38)</h3>
-<ul>
-  <li><strong>系统监控</strong>:内存缓冲,每日 23:20 一次写入,SD 卡写入从 2880 次/天降至 1 次</li>
-  <li><strong>运行日志</strong>:内存环形缓冲区,Web 日志页面零磁盘 I/O,每日 23:20 写入 <code>PlateRecApp_YYYYMMDD.log</code></li>
-  <li><strong>旧版日志清理</strong>:内存模式启用时自动跳过旧版 copytruncate,避免无意义读写</li>
-  <li><strong>退出安全刷盘</strong>:SIGTERM/SIGINT 时立即执行最后一次刷盘</li>
-</ul>
-
-<h3>日志管理(v36 内存缓冲)</h3>
-<ul>
-  <li><strong>内存缓冲模式</strong>(<code>enabled=1</code>,默认):所有日志写入内存环形缓冲区,Web 日志页面直接从内存读取,零磁盘 I/O</li>
-  <li><strong>直写文件模式</strong>(<code>enabled=0</code>):传统模式,日志直接写入文件</li>
-  <li><strong>定时刷盘</strong>:每日 <code>flush_time</code>(默认 23:20)自动将内存日志批量写入 <code>PlateRecApp_YYYYMMDD.log</code></li>
-  <li><strong>退出刷盘</strong>:<code>auto_flush_on_exit=1</code> 时,收到 SIGTERM/SIGINT 信号立即刷盘,防止日志丢失</li>
-  <li><strong>stdout 捕获</strong>:<code>redirect_stdout=1</code> 时,stdout/stderr 通过 pipe 重定向到内存缓冲区统一收集</li>
-  <li>Web 界面实时查看日志(支持 Tail 模式),显示内存使用率和缓冲状态</li>
-  <li>超期日志文件自动清理(由 <code>retention_days</code> 控制),支持手动触发刷盘</li>
-</ul>
-<div class="note">两个子系统的 <code>flush_time</code> 均配置为 23:20,分别在 <code>[log]</code> 和 <code>[system]</code> 段中设置,可独立调整。</div>
-</div>
-</div>
-
-<!-- 15. Web管理 -->
-<div class="card" id="sec15">
-<div class="card-title" onclick="toggleCard(this)">15. Web 管理功能 <span class="arrow">▼</span></div>
-<div class="card-body">
-<table>
-  <tr><th>页面</th><th>路径</th><th>权限</th><th>说明</th></tr>
-  <tr><td>监控首页</td><td>/</td><td>所有用户</td><td>实时监控、识别记录、连接状态、ROI设置</td></tr>
-  <tr><td>视频预览</td><td>/video</td><td>所有用户</td><td>实时视频流</td></tr>
-  <tr><td>称重记录</td><td>/weight</td><td>所有用户</td><td>称重数据</td></tr>
-  <tr><td>锁定管理</td><td>/locks</td><td>所有用户</td><td>交替锁定记录</td></tr>
-  <tr><td>帮助手册</td><td>/help</td><td>超级管理员</td><td>本文档</td></tr>
-  <tr><td>系统配置</td><td>/config</td><td>管理员+</td><td>config.ini 在线编辑</td></tr>
-  <tr><td>系统监控</td><td>/system</td><td>超级管理员</td><td>CPU/内存/温度/磁盘</td></tr>
-  <tr><td>frpc 管理</td><td>/frpc</td><td>超级管理员</td><td>内网穿透状态和配置</td></tr>
-  <tr><td>SSH 密钥</td><td>/ssh-keys</td><td>超级管理员</td><td>SSH 公钥管理</td></tr>
-  <tr><td>日志管理</td><td>/logs</td><td>超级管理员</td><td>系统日志查看</td></tr>
-  <tr><td>登录</td><td>/login</td><td>公开</td><td>用户登录页面</td></tr>
-</table>
-
-<h3>Tab 分离:实时数据 + 历史查询(v41 新增)</h3>
-<p>首页和称重页均改为 Tab 分离结构,自动刷新与手动查询完全独立:</p>
-<table>
-  <tr><th>Tab</th><th>数据来源</th><th>刷新方式</th></tr>
-  <tr><td>📡 实时数据 / 实时称重</td><td>固定最新 20 条,无条件加载</td><td>每 30 秒自动刷新</td></tr>
-  <tr><td>🔍 历史查询</td><td>按筛选条件查询,支持完整分页</td><td>手动触发,不自动刷新</td></tr>
-</table>
-<p>两个 Tab 使用独立的状态和函数,互不干扰。切换 Tab 时自动加载对应数据;重试操作后根据当前 Tab 智能刷新对应数据源。</p>
-
-<h3>分类查询筛选条件(v41)</h3>
-<p>历史查询 Tab 支持多维度组合筛选(所有条件 AND 组合):</p>
-<table>
-  <tr><th>筛选条件</th><th>首页</th><th>称重页</th><th>说明</th></tr>
-  <tr><td>车牌号</td><td>✅</td><td>✅</td><td>模糊匹配,如"沪A"</td></tr>
-  <tr><td>联单编号</td><td>✅</td><td>✅</td><td>模糊匹配</td></tr>
-  <tr><td>方向</td><td>✅</td><td>✅</td><td>全部 / 进站 / 出站</td></tr>
-  <tr><td>上传状态</td><td>✅</td><td>✅</td><td>首页:成功/失败;称重页多一个"待上传"(status=0)</td></tr>
-  <tr><td>飞书状态</td><td>✅</td><td>—</td><td>全部 / 已发送 / 失败 / 未发送</td></tr>
-  <tr><td>日期范围</td><td>✅</td><td>✅</td><td>按抓拍时间过滤(YYYY-MM-DD)</td></tr>
-</table>
-
-<h3>分页控件(v41)</h3>
-<ul>
-  <li><strong>每页条数</strong>:可选 20 / 50 / 100 条</li>
-  <li><strong>页码导航</strong>:首页 / 上一页 / 页码(当前页前后各 2 页,含首末页和省略号)/ 下一页 / 末页</li>
-  <li><strong>跳转</strong>:输入页码 + GO 按钮快速跳转</li>
-  <li><strong>信息栏</strong>:显示"第 X/Y 页 · 共 Z 条"</li>
-</ul>
-</div>
-</div>
-
-<!-- 16. 数据库 -->
-<div class="card" id="sec16">
-<div class="card-title" onclick="toggleCard(this)">16. 数据库与备份 <span class="arrow">▼</span></div>
-<div class="card-body">
-<h3>数据库文件</h3>
-<table>
-  <tr><th>文件</th><th>路径</th><th>说明</th></tr>
-  <tr><td>认证数据库</td><td>data/auth.db</td><td>用户账号、密码、Session</td></tr>
-  <tr><td>识别记录</td><td>data/upload_records.db</td><td>车牌识别上传记录</td></tr>
-  <tr><td>系统监控</td><td>data/system_metrics.db</td><td>性能监控时序数据(原始+小时聚合+日聚合)</td></tr>
-</table>
-<div class="tip">v29 版本起,所有数据库统一存放在 <code>data/</code> 目录下。系统首次启动时自动将旧路径数据库迁移到新位置(旧文件保留为备份)。</div>
-
-<h3>upload_records 表结构</h3>
-<table>
-  <tr><th>字段</th><th>说明</th></tr>
-  <tr><td>id</td><td>自增主键</td></tr>
-  <tr><td>create_time</td><td>记录创建时间</td></tr>
-  <tr><td>capture_time</td><td>抓拍时间(用于时间对齐)</td></tr>
-  <tr><td>plate_number</td><td>车牌号</td></tr>
-  <tr><td>tb_num</td><td>联单编号</td></tr>
-  <tr><td>station_type</td><td>方向:1=进站,2=出站</td></tr>
-  <tr><td>lo_photo_path / hi_photo_path</td><td>照片文件名(相对 PlateJPG/),清理后为 NULL</td></tr>
-  <tr><td>lo_upload_status / hi_upload_status</td><td>上传状态:0=待传,1=成功,2=失败</td></tr>
-  <tr><td>feishu_status</td><td>飞书通知:1=已发送,2=失败,3=未发送</td></tr>
-  <tr><td>retry_count</td><td>重试次数(上限 5 次)</td></tr>
-</table>
-
-<h3>备份建议</h3>
-<ul>
-  <li>定期备份 <code>data/</code> 目录和 <code>config.ini</code></li>
-  <li>nginx SSL 证书和配置文件备份:<code>/etc/nginx/ssl/</code>、<code>/etc/nginx/conf.d/</code></li>
-  <li>frpc 配置修改时系统自动备份</li>
-  <li>照片数据在 <code>PhotoMaxCapacityMB</code> 限制内自动管理,重要照片建议另行备份</li>
-</ul>
-</div>
-</div>
-
-<!-- 17. FAQ -->
-<div class="card" id="sec17">
-<div class="card-title" onclick="toggleCard(this)">17. 常见问题 FAQ <span class="arrow">▼</span></div>
-<div class="card-body">
-
-<h3>Q: HTTP 访问没有跳转到 HTTPS?</h3>
-<p>A: 检查以下几点:</p>
-<ul>
-  <li>确认 nginx stream 模块已安装:<code>nginx -V 2>&amp;1 | grep stream</code></li>
-  <li>确认 nginx 配置正确:<code>sudo nginx -t</code></li>
-  <li>确认 8081 端口正在监听:<code>ss -tlnp | grep 8081</code></li>
-  <li>确认 frpc 隧道 <code>plate_web</code> 正常运行(localPort 为 40963)</li>
-  <li>浏览器可能缓存了旧的重定向,尝试清除缓存或无痕模式</li>
-  <li>测试本机重定向:<code>curl -I http://localhost:40963/</code>,应返回 301</li>
-</ul>
-
-<h3>Q: HTTPS 访问返回错误?</h3>
-<p>A: v2 方案下 TLS 由 C++ 应用处理,检查:</p>
-<ul>
-  <li>PlateRecApp 是否正在运行:<code>systemctl status PlateRecApp</code></li>
-  <li>PlateRecApp 是否启用了 HTTPS:<code>config.ini [ssl] enabled=1</code></li>
-  <li>SSL 证书文件是否存在:<code>ls -l ssl/plate_fullchain.pem ssl/plate_privkey.pem</code></li>
-  <li>nginx stream 是否正常监听:<code>ss -tlnp | grep 40963</code></li>
-  <li>nginx 错误日志:<code>sudo tail -f /var/log/nginx/error.log</code></li>
-</ul>
-
-<h3>Q: <code>unknown directive "stream"</code>?</h3>
-<p>A: stream 模块未加载。执行:<code>sudo apt install -y libnginx-mod-stream</code>,确认 nginx.conf 顶部有 <code>load_module modules/ngx_stream_module.so;</code></p>
-
-<h3>Q: frpc 重启后隧道不生效?</h3>
-<p>A: 检查 frpc.toml 配置语法:</p>
-<pre>frpc verify -c /path/to/frpc.toml</pre>
-<p>常见错误:TOML 格式问题(缺少引号、缩进错误)。通过 Web 管理界面编辑配置时系统会自动校验。</p>
-
-<h3>Q: 自签名证书浏览器警告怎么消除?</h3>
-<p>A: 自签名证书无法消除浏览器警告(这是正常安全行为)。解决方案:</p>
-<ul>
-  <li>方式1:使用 Let's Encrypt 免费证书(需域名解析到公网 IP)</li>
-  <li>方式2:在客户端将自签名 CA 证书添加到受信任的根证书列表</li>
-  <li>方式3:忽略警告,继续使用(加密通信正常,仅证书不受信任)</li>
-</ul>
-
-<h3>Q: 摄像头连接失败怎么办?</h3>
-<p>A: 确认 RTSP 地址格式正确,在 VLC 中测试播放。尝试 <code>rtsp_transport=tcp</code>。</p>
-
-<h3>Q: in_out_interval 填 5 是 5 秒还是 5 分钟?</h3>
-<p>A: <strong>5 分钟</strong>。单位是分钟,系统内部自动 ×60 转为秒。</p>
-
-<h3>Q: 照片被自动清理后,Web 页面看到"已清理"怎么办?</h3>
-<p>A: 这是正常现象。当 <code>PlateJPG/</code> 目录总大小超过 <code>PhotoMaxCapacityMB</code>(默认 1024MB)时,系统每日凌晨 2:00 自动删除最旧的照片释放空间。被删除照片对应的数据库记录仍保留(业务数据不丢失),Web 页面显示灰色「已清理」占位。如需保留更多照片,调大该配置值。</p>
-
-<h3>Q: 日志中出现 <code>[路径校验] 转换绝对路径失败</code> 错误?</h3>
-<p>A: 此错误在旧版本(v41 修复前)出现,原因是照片被物理删除后 DB 路径字段未同步清空,前端仍请求已删除文件。v41 已修复:删除照片时通过 basename 匹配同步清空 DB 路径字段,启动时自动扫描孤儿路径,且 <code>/photo</code> 接口对缺失文件静默返回 404 不再打 ERROR 日志。升级后此错误不再出现。</p>
-
-<h3>Q: 如何调整照片存储上限?</h3>
-<p>A: 修改 <code>config.ini</code> 的 <code>[config]</code> 段中 <code>PhotoMaxCapacityMB</code> 值并重启服务。例如:</p>
-<pre>[config]
-PhotoMaxCapacityMB=2048   # 调整为 2GB</pre>
-
-<h3>Q: 称重页的"待上传"状态是什么意思?</h3>
-<p>A: 称重记录上传状态筛选比首页多一个「待上传」选项,对应数据库中 <code>upload_status = 0</code> 的记录,即称重数据尚未成功上传到云端 API,可通过此筛选快速定位需重试的记录。</p>
-
-<h3>Q: 如何重置管理员密码?</h3>
-<pre>cd /opt/openAI/project/003.PlateRecAPP
-python3 reset_passwords.py</pre>
-</div>
-</div>
-
-</div>
-
-<script>
-function toggleCard(el) {
-  el.classList.toggle('collapsed');
-  var body = el.nextElementSibling;
-  body.classList.toggle('hidden');
-}
-</script>
-</body>
-</html>

+ 0 - 284
assets/web/index.bak.html

@@ -1,284 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v26</p>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 3 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;">' + rec.tb_num.substring(0, 15) + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                if (data.success) {
-                    alert('重试请求已提交');
-                    loadMessages(currentPage, currentKeyword);
-                } else {
-                    alert('重试失败');
-                }
-            })
-            .catch(e => alert('重试请求失败'));
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 310
assets/web/index.bak2.html

@@ -1,310 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix9</p>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;">' + rec.tb_num.substring(0, 22) + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 317
assets/web/index.bak3.html

@@ -1,317 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix19</p>
-        <style>.video-link:hover{background:rgba(255,255,255,0.35)!important;}</style>
-        <div class="video-links" style="margin-top:12px;display:flex;justify-content:center;gap:10px;flex-wrap:wrap;">
-            <a href="/video?stream=in-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位进站</a>
-            <a href="/video?stream=in-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位进站</a>
-            <a href="/video?stream=out-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位出站</a>
-            <a href="/video?stream=out-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位出站</a>
-        </div>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;">' + rec.tb_num.substring(0, 22) + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 317
assets/web/index.bak4..html

@@ -1,317 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix19</p>
-        <style>.video-link:hover{background:rgba(255,255,255,0.35)!important;}</style>
-        <div class="video-links" style="margin-top:12px;display:flex;justify-content:center;gap:10px;flex-wrap:wrap;">
-            <a href="/video?stream=in-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位进站</a>
-            <a href="/video?stream=in-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位进站</a>
-            <a href="/video?stream=out-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位出站</a>
-            <a href="/video?stream=out-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位出站</a>
-        </div>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;font-size:12px;">' + rec.tb_num + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 317
assets/web/index.bak4.html

@@ -1,317 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix19</p>
-        <style>.video-link:hover{background:rgba(255,255,255,0.35)!important;}</style>
-        <div class="video-links" style="margin-top:12px;display:flex;justify-content:center;gap:10px;flex-wrap:wrap;">
-            <a href="/video?stream=in-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位进站</a>
-            <a href="/video?stream=in-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位进站</a>
-            <a href="/video?stream=out-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位出站</a>
-            <a href="/video?stream=out-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位出站</a>
-        </div>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;font-size:12px;">' + rec.tb_num + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 475
assets/web/index.bak5.html

@@ -1,475 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        /* 用户信息栏 */
-        .user-bar { background: rgba(255,255,255,0.15); padding: 8px 20px; display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
-        .user-bar a { color: rgba(255,255,255,0.85); text-decoration: none; margin-left: 12px; padding: 3px 8px; border-radius: 4px; transition: background 0.2s; }
-        .user-bar a:hover { background: rgba(255,255,255,0.2); color: white; }
-        .user-bar .user-info { color: white; font-weight: 500; }
-        .user-bar .role-badge { background: rgba(255,215,0,0.3); color: #ffd700; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin-left: 6px; }
-        .user-bar .logout-btn { color: rgba(255,255,255,0.7); cursor: pointer; border: 1px solid rgba(255,255,255,0.3); padding: 2px 10px; border-radius: 4px; background: transparent; font-size: 12px; }
-        .user-bar .logout-btn:hover { background: rgba(255,255,255,0.15); color: white; }
-        /* 连接状态面板 */
-        .conn-panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin: 0 20px 0 20px; max-width: 1360px; }
-        .conn-panel h2 { font-size: 16px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 8px; }
-        .conn-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
-        .conn-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; }
-        .conn-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
-        .conn-dot.online { background: #52c41a; box-shadow: 0 0 6px rgba(82,196,26,0.4); }
-        .conn-dot.offline { background: #f5222d; box-shadow: 0 0 6px rgba(245,34,46,0.4); }
-        .conn-dot.warn { background: #faad14; box-shadow: 0 0 6px rgba(250,173,20,0.4); }
-        .conn-dot.disabled { background: #d9d9d9; }
-        .conn-label { font-size: 13px; color: #333; font-weight: 500; }
-        .conn-detail { font-size: 11px; color: #999; margin-top: 2px; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix24</p>
-        <div class="user-bar" id="main-nav">
-            <div id="nav-links">
-                <a href="/">🏠 首页</a>
-                <a href="/video">🎥 视频</a>
-                <a href="/weight">⚖️ 称重</a>
-                <a href="/config" id="nav-config" style="display:none;">⚙️ 配置</a>
-                <a href="/locks" id="nav-locks" style="display:none;">🔒 锁定</a>
-                <a href="/system" id="nav-system" style="display:none;">📊 监控</a>
-                <a href="/frpc" id="nav-frpc" style="display:none;">🔗 隧道</a>
-                <a href="/ssh-keys" id="nav-ssh" style="display:none;">🔑 SSH</a>
-                <a href="/logs" id="nav-logs" style="display:none;">📋 日志</a>
-            </div>
-            <div id="user-info-area">
-                <span class="user-info" id="user-display">未登录</span>
-            </div>
-        </div>
-    </div>
-    
-    <!-- 连接状态面板 -->
-    <div class="conn-panel" style="margin:20px auto;max-width:1360px;padding:0 20px;">
-        <div style="background:white;border-radius:10px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1);">
-            <h2 style="font-size:16px;color:#333;margin-bottom:15px;border-bottom:2px solid #667eea;padding-bottom:8px;">🔌 连接状态</h2>
-            <div class="conn-grid" id="connections-grid">
-                <div class="conn-item"><div class="conn-dot disabled"></div><div><div class="conn-label">加载中...</div></div></div>
-            </div>
-        </div>
-    </div>
-
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        function getCookie(n){let m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;font-size:12px;">' + rec.tb_num + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCookie('csrf_token') }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // ==================== 用户信息显示 + 导航栏权限控制 ====================
-        function loadUserInfo() {
-            fetch('/api/auth/status')
-                .then(r => r.json())
-                .then(data => {
-                    const area = document.getElementById('user-info-area');
-                    if (data.authenticated) {
-                        const roles = {0: '普通用户', 1: '管理员', 2: '超级管理员'};
-                        const roleName = roles[data.role] || '未知角色';
-                        const roleClass = data.role === 2 ? 'style="background:rgba(255,215,0,0.3);color:#ffd700;"' : 
-                                          data.role === 1 ? 'style="background:rgba(64,169,255,0.3);color:#40a9ff;"' : '';
-                        area.innerHTML = '<span class="user-info">👤 ' + data.username + '</span>' +
-                            '<span class="role-badge" ' + roleClass + '>' + roleName + '</span>' +
-                            ' <button class="logout-btn" onclick="logout()">退出</button>';
-
-                        // 根据角色显示导航栏项目
-                        // role=0: 首页、视频、称重(已默认显示)
-                        // role=1: + 配置、锁定
-                        // role=2: + 监控、隧道、SSH、日志
-                        if (data.role >= 1) {
-                            show('nav-config');
-                            show('nav-locks');
-                        }
-                        if (data.role >= 2) {
-                            show('nav-system');
-                            show('nav-frpc');
-                            show('nav-ssh');
-                            show('nav-logs');
-                        }
-                    } else {
-                        area.innerHTML = '<a href="/login" style="color:white;text-decoration:underline;">登录</a>';
-                    }
-                })
-                .catch(e => console.error('用户信息加载失败:', e));
-        }
-
-        function show(id) {
-            var el = document.getElementById(id);
-            if (el) el.style.display = '';
-        }
-
-        function logout() {
-            fetch('/api/auth/logout', {method: 'POST', headers: {'X-CSRF-Token': getCookie('csrf_token')}})
-                .then(() => { window.location.href = '/login'; })
-                .catch(() => { window.location.href = '/login'; });
-        }
-
-        // ==================== 连接状态显示 ====================
-        function loadConnections() {
-            fetch('/api/connections')
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-
-                    // RTSP 摄像头
-                    if (data.rtsp) {
-                        data.rtsp.forEach(cam => {
-                            let dot = 'disabled';
-                            let detail = '未配置';
-                            if (cam.configured && cam.running) { dot = 'online'; detail = '采集中'; }
-                            else if (cam.configured && !cam.running) { dot = 'offline'; detail = '已断开'; }
-                            else { dot = 'disabled'; detail = '未配置'; }
-                            html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                                '<div><div class="conn-label">📷 ' + cam.name + '</div>' +
-                                '<div class="conn-detail">' + detail + '</div></div></div>';
-                        });
-                    }
-
-                    // MQTT
-                    if (data.mqtt) {
-                        let dot = 'disabled', detail = '未启用';
-                        if (data.mqtt.enabled) {
-                            if (data.mqtt.status === 1) { dot = 'online'; detail = data.mqtt.state + ' (' + data.mqtt.host + ')'; }
-                            else if (data.mqtt.status === 2) { dot = 'warn'; detail = data.mqtt.state + '...'; }
-                            else { dot = 'offline'; detail = data.mqtt.state; }
-                        }
-                        html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                            '<div><div class="conn-label">📨 MQTT</div>' +
-                            '<div class="conn-detail">' + detail + '</div></div></div>';
-                    }
-
-                    // 数据库
-                    if (data.database) {
-                        let dot = data.database.connected ? 'online' : 'offline';
-                        let detail = data.database.connected ? 'SQLite 已连接' : '未连接';
-                        html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                            '<div><div class="conn-label">🗄️ 数据库</div>' +
-                            '<div class="conn-detail">' + detail + '</div></div></div>';
-                    }
-
-                    // frpc
-                    if (data.frpc) {
-                        let dot = data.frpc.running ? 'online' : 'offline';
-                        let detail = data.frpc.running ? (data.frpc.tunnels + ' 条隧道活跃') : '未运行';
-                        html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                            '<div><div class="conn-label">🔗 frpc 隧道</div>' +
-                            '<div class="conn-detail">' + detail + (data.frpc.version ? ' v' + data.frpc.version : '') + '</div></div></div>';
-                    }
-
-                    // Web 服务器
-                    if (data.web) {
-                        let detail = '端口 ' + data.web.port + (data.web.https ? ' (HTTPS)' : '');
-                        html += '<div class="conn-item"><div class="conn-dot online"></div>' +
-                            '<div><div class="conn-label">🌐 Web 服务</div>' +
-                            '<div class="conn-detail">' + detail + '</div></div></div>';
-                    }
-
-                    document.getElementById('connections-grid').innerHTML = html;
-                })
-                .catch(e => {
-                    console.error('连接状态加载失败:', e);
-                    document.getElementById('connections-grid').innerHTML = 
-                        '<div class="conn-item"><div class="conn-dot offline"></div><div><div class="conn-label">加载失败</div></div></div>';
-                });
-        }
-
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        loadUserInfo();
-        loadConnections();
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 713
assets/web/index.html

@@ -1,713 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        /* fix24 v41: Tab 切换样式 */
-        .tab-bar { display: flex; gap: 0; margin-bottom: 15px; border-bottom: 2px solid #e9ecef; }
-        .tab-btn { padding: 10px 20px; background: transparent; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 14px; color: #666; transition: all 0.2s; }
-        .tab-btn:hover { color: #667eea; background: #f8f9fa; }
-        .tab-btn.active { color: #667eea; border-bottom-color: #667eea; font-weight: 600; }
-        .tab-content { display: none; }
-        .tab-content.active { display: block; }
-        /* fix24 v41: 分类查询样式 */
-        .filter-bar { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 12px; margin-bottom: 15px; }
-        .filter-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
-        .filter-row + .filter-row { margin-top: 8px; }
-        .filter-item { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 130px; }
-        .filter-item label { font-size: 12px; color: #666; font-weight: 500; }
-        .filter-item input, .filter-item select { padding: 7px 9px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; background: white; }
-        .filter-item input:focus, .filter-item select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 2px rgba(102,126,234,0.15); }
-        .filter-buttons { flex: 0 0 auto; flex-direction: row !important; gap: 8px !important; }
-        .btn-search, .btn-reset { padding: 7px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; white-space: nowrap; }
-        .btn-search { background: #667eea; color: white; }
-        .btn-search:hover { background: #764ba2; }
-        .btn-reset { background: white; color: #666; border: 1px solid #d9d9d9; }
-        .btn-reset:hover { border-color: #667eea; color: #667eea; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-cleaned { color: #999; font-size: 12px; font-style: italic; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 15px; }
-        .pagination button { padding: 6px 12px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; font-size: 13px; }
-        .pagination button:hover:not(:disabled) { background: #f0f5ff; border-color: #667eea; color: #667eea; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination button.active { background: #667eea; color: white; border-color: #667eea; }
-        .pagination .info { color: #666; font-size: 13px; padding: 0 4px; }
-        .pagination-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; flex-wrap: wrap; gap: 10px; }
-        .pagination-left, .pagination-right { display: flex; align-items: center; gap: 5px; font-size: 13px; color: #666; }
-        .pagination-left select, .pagination-right input { padding: 5px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; width: 65px; }
-        .pagination-right input { width: 55px; }
-        .pagination-right button { padding: 5px 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
-        .pagination-right button:hover { background: #764ba2; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        .user-bar { background: rgba(255,255,255,0.15); padding: 8px 20px; display: flex; justify-content: center; align-items: center; font-size: 13px; position: relative; }
-        .user-bar a { color: rgba(255,255,255,0.85); text-decoration: none; padding: 5px 12px; border-radius: 4px; transition: background 0.2s; }
-        .user-bar a:hover { background: rgba(255,255,255,0.2); color: white; }
-        .user-bar .user-info { color: white; font-weight: 500; }
-        .user-bar .role-badge { background: rgba(255,215,0,0.3); color: #ffd700; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin-left: 6px; }
-        .user-bar .logout-btn { color: rgba(255,255,255,0.7); cursor: pointer; border: 1px solid rgba(255,255,255,0.3); padding: 2px 10px; border-radius: 4px; background: transparent; font-size: 12px; }
-        .user-bar .logout-btn:hover { background: rgba(255,255,255,0.15); color: white; }
-        #nav-links { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
-        #user-info-area { position: absolute; right: 20px; top: 50%; transform: translateY(-50%); }
-        .conn-panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin: 0 20px 0 20px; max-width: 1360px; }
-        .conn-panel h2 { font-size: 16px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 8px; }
-        .conn-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
-        .conn-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; }
-        .conn-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
-        .conn-dot.online { background: #52c41a; box-shadow: 0 0 6px rgba(82,196,26,0.4); }
-        .conn-dot.offline { background: #f5222d; box-shadow: 0 0 6px rgba(245,34,46,0.4); }
-        .conn-dot.warn { background: #faad14; box-shadow: 0 0 6px rgba(250,173,20,0.4); }
-        .conn-dot.disabled { background: #d9d9d9; }
-        .conn-label { font-size: 13px; color: #333; font-weight: 500; }
-        .conn-detail { font-size: 11px; color: #999; margin-top: 2px; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-        @media (max-width: 768px) {
-            .header { padding: 12px; }
-            .header h1 { font-size: 18px; }
-            .header p { font-size: 11px; }
-            .user-bar { flex-direction: column; gap: 8px; padding: 8px 12px; }
-            #user-info-area { position: static; transform: none; text-align: center; }
-            #nav-links { gap: 4px; }
-            .user-bar a { padding: 4px 8px; font-size: 12px; }
-            .conn-panel { margin: 10px; padding: 0; }
-            .conn-grid { grid-template-columns: 1fr 2fr; gap: 8px; }
-            .container { flex-direction: column; padding: 10px; gap: 10px; }
-            .report-panel { width: 100%; }
-            .panel { padding: 12px; }
-            .panel h2 { font-size: 15px; }
-            table { font-size: 11px; }
-            th, td { padding: 6px 4px; }
-            .photo-thumb { width: 40px; height: 28px; }
-            .tab-btn { padding: 8px 12px; font-size: 12px; }
-        }
-        @media (max-width: 480px) {
-            .conn-grid { grid-template-columns: 1fr; }
-            .header h1 { font-size: 16px; }
-            #nav-links a { font-size: 11px; padding: 3px 6px; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix24</p>
-        <div class="user-bar" id="main-nav">
-            <div id="nav-links">
-                <a href="/">🏠 首页</a>
-                <a href="/video">🎥 视频</a>
-                <a href="/weight">⚖️ 称重</a>
-                <a href="/config" id="nav-config" style="display:none;">⚙️ 配置</a>
-                <a href="/locks" id="nav-locks">🔒 锁定</a>
-                <a href="/system" id="nav-system" style="display:none;">📊 监控</a>
-                <a href="/frpc" id="nav-frpc" style="display:none;">🔗 隧道</a>
-                <a href="/ssh-keys" id="nav-ssh" style="display:none;">🔑 SSH</a>
-                <a href="/logs" id="nav-logs" style="display:none;">📋 日志</a>
-                <a href="/help" id="nav-help" style="display:none;">📖 帮助</a>
-            </div>
-            <div id="user-info-area">
-                <span class="user-info" id="user-display">未登录</span>
-            </div>
-        </div>
-    </div>
-    
-    <div class="conn-panel" style="margin:20px auto;max-width:1360px;padding:0 20px;">
-        <div style="background:white;border-radius:10px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1);">
-            <h2 style="font-size:16px;color:#333;margin-bottom:15px;border-bottom:2px solid #667eea;padding-bottom:8px;">🔌 连接状态</h2>
-            <div class="conn-grid" id="connections-grid">
-                <div class="conn-item"><div class="conn-dot disabled"></div><div><div class="conn-label">加载中...</div></div></div>
-            </div>
-        </div>
-    </div>
-
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>识别记录</h2>
-            <!-- fix24 v41: Tab 切换 -->
-            <div class="tab-bar">
-                <button class="tab-btn active" onclick="switchTab('realtime')">📡 实时数据</button>
-                <button class="tab-btn" onclick="switchTab('history')">🔍 历史查询</button>
-            </div>
-            
-            <!-- 实时数据 Tab -->
-            <div id="tab-realtime" class="tab-content active">
-                <div style="background:#e6f7ff;border:1px solid #91d5ff;border-radius:6px;padding:10px;margin-bottom:15px;font-size:13px;color:#0050b3;">
-                    💡 实时数据:显示最新 20 条记录,每 30 秒自动刷新
-                </div>
-                <div class="table-container">
-                    <table>
-                        <thead>
-                            <tr>
-                                <th>编号</th>
-                                <th>日期时间</th>
-                                <th>车牌号</th>
-                                <th>联单编号</th>
-                                <th>类型</th>
-                                <th>低位照片</th>
-                                <th>高位照片</th>
-                                <th>上传状态</th>
-                                <th>飞书状态</th>
-                                <th>操作</th>
-                            </tr>
-                        </thead>
-                        <tbody id="realtime-body">
-                            <tr><td colspan="10" class="loading">加载中...</td></tr>
-                        </tbody>
-                    </table>
-                </div>
-                <div class="refresh-info" id="realtime-refresh-info">数据自动刷新间隔: 30秒</div>
-            </div>
-            
-            <!-- 历史查询 Tab -->
-            <div id="tab-history" class="tab-content">
-                <div class="filter-bar">
-                    <div class="filter-row">
-                        <div class="filter-item">
-                            <label>车牌号</label>
-                            <input type="text" id="filter-plate" placeholder="如 沪EQ3803">
-                        </div>
-                        <div class="filter-item">
-                            <label>联单编号</label>
-                            <input type="text" id="filter-tbnum" placeholder="联单编号">
-                        </div>
-                        <div class="filter-item">
-                            <label>类型</label>
-                            <select id="filter-station">
-                                <option value="0">全部</option>
-                                <option value="1">进站</option>
-                                <option value="2">出站</option>
-                            </select>
-                        </div>
-                    </div>
-                    <div class="filter-row">
-                        <div class="filter-item">
-                            <label>开始日期</label>
-                            <input type="date" id="filter-date-from">
-                        </div>
-                        <div class="filter-item">
-                            <label>结束日期</label>
-                            <input type="date" id="filter-date-to">
-                        </div>
-                        <div class="filter-item">
-                            <label>上传状态</label>
-                            <select id="filter-upload">
-                                <option value="0">全部</option>
-                                <option value="1">成功</option>
-                                <option value="2">失败</option>
-                            </select>
-                        </div>
-                        <div class="filter-item">
-                            <label>飞书状态</label>
-                            <select id="filter-feishu">
-                                <option value="0">全部</option>
-                                <option value="1">已发送</option>
-                                <option value="2">失败</option>
-                                <option value="3">未发送</option>
-                            </select>
-                        </div>
-                        <div class="filter-item filter-buttons">
-                            <button class="btn-search" onclick="searchHistory(1)">🔍 查询</button>
-                            <button class="btn-reset" onclick="resetFilters()">重置</button>
-                        </div>
-                    </div>
-                </div>
-                <div class="table-container">
-                    <table>
-                        <thead>
-                            <tr>
-                                <th>编号</th>
-                                <th>日期时间</th>
-                                <th>车牌号</th>
-                                <th>联单编号</th>
-                                <th>类型</th>
-                                <th>低位照片</th>
-                                <th>高位照片</th>
-                                <th>上传状态</th>
-                                <th>飞书状态</th>
-                                <th>操作</th>
-                            </tr>
-                        </thead>
-                        <tbody id="history-body">
-                            <tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">请设置查询条件后点击"查询"按钮</td></tr>
-                        </tbody>
-                    </table>
-                </div>
-                <div class="pagination-bar">
-                    <div class="pagination-left">
-                        <span>每页</span>
-                        <select id="page-size" onchange="changePageSize()">
-                            <option value="20">20</option>
-                            <option value="50">50</option>
-                            <option value="100">100</option>
-                        </select>
-                        <span>条</span>
-                    </div>
-                    <div class="pagination" id="pagination"></div>
-                    <div class="pagination-right">
-                        <span>跳至</span>
-                        <input type="number" id="jump-page" min="1" value="1">
-                        <span>页</span>
-                        <button onclick="jumpToPage()">GO</button>
-                    </div>
-                </div>
-                <div class="refresh-info" id="history-refresh-info">历史查询:手动触发,不自动刷新</div>
-            </div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentPageSize = 20;
-        let currentTotal = 0;
-        let currentTotalPages = 0;
-        let currentFilters = {};
-        let currentTab = 'realtime';
-        function getCookie(n){let m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}
-        
-        // fix24 v41: Tab 切换
-        function switchTab(tab) {
-            currentTab = tab;
-            document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
-            document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
-            
-            if (tab === 'realtime') {
-                document.querySelector('.tab-btn:nth-child(1)').classList.add('active');
-                document.getElementById('tab-realtime').classList.add('active');
-                loadRealtimeData();
-            } else {
-                document.querySelector('.tab-btn:nth-child(2)').classList.add('active');
-                document.getElementById('tab-history').classList.add('active');
-            }
-        }
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric"><span class="metric-label">createBill成功率</span><span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span></div>
-                        <div class="metric"><span class="metric-label">createBill总调用</span><span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span></div>
-                        <div class="metric"><span class="metric-label">照片平均耗时</span><span class="metric-value">${data.avg_upload_time}ms</span></div>
-                        <div class="metric"><span class="metric-label">TIME_WINDOW命中</span><span class="metric-value">${data.time_window_hits}</span></div>
-                        <div class="metric"><span class="metric-label">进站缓存命中率</span><span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span></div>
-                        <div class="metric"><span class="metric-label">出站缓存命中率</span><span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span></div>
-                        <div class="metric"><span class="metric-label">进站统计</span><span class="metric-value">${data.in_station_count}</span></div>
-                        <div class="metric"><span class="metric-label">出站统计</span><span class="metric-value">${data.out_station_count}</span></div>
-                        <div class="metric"><span class="metric-label">总记录数</span><span class="metric-value">${data.total_records}</span></div>
-                        <div class="metric"><span class="metric-label">失败记录</span><span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span></div>
-                        <div class="metric"><span class="metric-label">飞书成功数</span><span class="metric-value">${data.feishu_success_count}</span></div>
-                        <div class="metric"><span class="metric-label">系统运行时间</span><span class="metric-value">${data.uptime}</span></div>
-                        <div class="metric"><span class="metric-label">网络超时</span><span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span></div>
-                        <div class="metric"><span class="metric-label">服务器错误</span><span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span></div>
-                        <div class="metric"><span class="metric-label">总上传错误</span><span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span></div>
-                        <div class="metric"><span class="metric-label">交替锁定</span><span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span></div>
-                        <div class="metric"><span class="metric-label">紧急告警抑制</span><span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span></div>
-                        <div class="metric"><span class="metric-label">交替锁定间隔</span><span class="metric-value">${data.in_out_interval_sec}秒</span></div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        // fix24 v41: 实时数据加载(固定page_size=20)
-        function loadRealtimeData() {
-            fetch('/api/messages?page=1&page_size=20')
-                .then(r => r.json())
-                .then(data => {
-                    renderMessages(data, 'realtime-body');
-                    const refreshEl = document.getElementById('realtime-refresh-info');
-                    refreshEl.textContent = '数据自动刷新间隔: 30秒(共 ' + (data.total || 0) + ' 条,显示最新 20 条)';
-                })
-                .catch(e => {
-                    console.error('实时数据加载失败:', e);
-                    document.getElementById('realtime-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        // fix24 v41: 历史查询
-        function searchHistory(page) {
-            const filters = collectFilters();
-            if (filters.date_from && filters.date_to && filters.date_from > filters.date_to) {
-                alert('开始日期不能晚于结束日期');
-                return;
-            }
-            currentFilters = filters;
-            loadHistoryData(page, filters);
-        }
-        
-        function loadHistoryData(page, filters) {
-            currentPage = page;
-            currentFilters = filters || {};
-            let params = new URLSearchParams();
-            params.set('page', page);
-            params.set('page_size', currentPageSize);
-            if (currentFilters.plate) params.set('plate', currentFilters.plate);
-            if (currentFilters.tb_num) params.set('tb_num', currentFilters.tb_num);
-            if (currentFilters.date_from) params.set('date_from', currentFilters.date_from);
-            if (currentFilters.date_to) params.set('date_to', currentFilters.date_to);
-            if (currentFilters.station_type) params.set('station_type', currentFilters.station_type);
-            if (currentFilters.upload_status) params.set('upload_status', currentFilters.upload_status);
-            if (currentFilters.feishu_status) params.set('feishu_status', currentFilters.feishu_status);
-
-            fetch('/api/messages?' + params.toString())
-                .then(r => r.json())
-                .then(data => {
-                    currentTotal = data.total || 0;
-                    currentTotalPages = data.total_pages || 0;
-                    renderMessages(data, 'history-body');
-                    renderPagination(data.page, currentTotalPages, currentTotal);
-                    document.getElementById('jump-page').value = data.page;
-                    document.getElementById('jump-page').max = currentTotalPages || 1;
-                    const refreshEl = document.getElementById('history-refresh-info');
-                    refreshEl.textContent = '历史查询结果:共 ' + currentTotal + ' 条(手动查询,不自动刷新)';
-                })
-                .catch(e => {
-                    console.error('历史查询失败:', e);
-                    document.getElementById('history-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">查询失败,请重试</td></tr>';
-                });
-        }
-        
-        function renderMessages(data, targetId) {
-            let html = '';
-            if (!data.records || data.records.length === 0) {
-                html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-            } else {
-                data.records.forEach(rec => {
-                    const stationType = rec.station_type === 1 ? '进站' : '出站';
-                    const uploadOk = rec.lo_upload_status === 1 && rec.hi_upload_status === 1;
-                    const uploadStatus = uploadOk ? '成功' : '失败';
-                    const uploadClass = uploadOk ? 'status-success' : 'status-failed';
-                    let feishuClass, feishuText;
-                    if (rec.feishu_status === 1) { feishuClass = 'status-success'; feishuText = '已发送'; }
-                    else if (rec.feishu_status === 2) { feishuClass = 'status-failed'; feishuText = '失败'; }
-                    else { feishuClass = 'status-pending'; feishuText = '未发送'; }
-                    const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-
-                    html += '<tr>';
-                    html += '<td>' + rec.id + '</td>';
-                    html += '<td>' + rec.create_time + '</td>';
-                    html += '<td><b>' + rec.plate_number + '</b></td>';
-                    html += '<td style="font-family:monospace;font-size:12px;">' + (rec.tb_num || '-') + '</td>';
-                    html += '<td>' + stationType + '</td>';
-                    html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)" onerror="this.outerHTML=\'<span class=photo-cleaned title=照片已被自动清理>已清理</span>\'">' : '<span class="photo-cleaned" title="照片已被自动清理">已清理</span>') + '</td>';
-                    html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)" onerror="this.outerHTML=\'<span class=photo-cleaned title=照片已被自动清理>已清理</span>\'">' : '<span class="photo-cleaned" title="照片已被自动清理">已清理</span>') + '</td>';
-                    html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                    html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                    html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ',event)" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                    html += '</tr>';
-                });
-            }
-            document.getElementById(targetId).innerHTML = html;
-        }
-
-        function collectFilters() {
-            return {
-                plate: document.getElementById('filter-plate').value.trim(),
-                tb_num: document.getElementById('filter-tbnum').value.trim(),
-                date_from: document.getElementById('filter-date-from').value,
-                date_to: document.getElementById('filter-date-to').value,
-                station_type: parseInt(document.getElementById('filter-station').value) || 0,
-                upload_status: parseInt(document.getElementById('filter-upload').value) || 0,
-                feishu_status: parseInt(document.getElementById('filter-feishu').value) || 0
-            };
-        }
-
-        function resetFilters() {
-            document.getElementById('filter-plate').value = '';
-            document.getElementById('filter-tbnum').value = '';
-            document.getElementById('filter-date-from').value = '';
-            document.getElementById('filter-date-to').value = '';
-            document.getElementById('filter-station').value = '0';
-            document.getElementById('filter-upload').value = '0';
-            document.getElementById('filter-feishu').value = '0';
-            currentFilters = {};
-            document.getElementById('history-body').innerHTML = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">请设置查询条件后点击"查询"按钮</td></tr>';
-            document.getElementById('pagination').innerHTML = '';
-            document.getElementById('history-refresh-info').textContent = '历史查询:手动触发,不自动刷新';
-        }
-
-        function renderPagination(page, totalPages, total) {
-            if (!totalPages || totalPages < 1) totalPages = 1;
-            let html = '';
-            html += '<button onclick="searchHistory(1)" ' + (page <= 1 ? 'disabled' : '') + '>首页</button>';
-            html += '<button onclick="searchHistory(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>';
-
-            let start = Math.max(1, page - 2);
-            let end = Math.min(totalPages, page + 2);
-            if (start > 1) {
-                html += '<button onclick="searchHistory(1)">1</button>';
-                if (start > 2) html += '<span class="info">…</span>';
-            }
-            for (let i = start; i <= end; i++) {
-                html += '<button onclick="searchHistory(' + i + ')" class="' + (i === page ? 'active' : '') + '">' + i + '</button>';
-            }
-            if (end < totalPages) {
-                if (end < totalPages - 1) html += '<span class="info">…</span>';
-                html += '<button onclick="searchHistory(' + totalPages + ')">' + totalPages + '</button>';
-            }
-
-            html += '<button onclick="searchHistory(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>';
-            html += '<button onclick="searchHistory(' + totalPages + ')" ' + (page >= totalPages ? 'disabled' : '') + '>末页</button>';
-            html += '<span class="info">第 ' + page + '/' + totalPages + ' 页 · 共 ' + total + ' 条</span>';
-            document.getElementById('pagination').innerHTML = html;
-        }
-
-        function changePageSize() {
-            currentPageSize = parseInt(document.getElementById('page-size').value) || 20;
-            if (currentTab === 'history' && hasActiveFilters(currentFilters)) {
-                searchHistory(1);
-            }
-        }
-
-        function jumpToPage() {
-            let p = parseInt(document.getElementById('jump-page').value) || 1;
-            if (p < 1) p = 1;
-            if (currentTotalPages && p > currentTotalPages) p = currentTotalPages;
-            searchHistory(p);
-        }
-        
-        function retryRecord(id, evt) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = evt ? evt.target : event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCookie('csrf_token') }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                if (currentTab === 'realtime') loadRealtimeData();
-                else if (hasActiveFilters(currentFilters)) loadHistoryData(currentPage, currentFilters);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        function hasActiveFilters(f) {
-            return f.plate || f.tb_num || f.date_from || f.date_to ||
-                   f.station_type !== 0 || f.upload_status !== 0 || f.feishu_status !== 0;
-        }
-        
-        // ==================== 用户信息显示 + 导航栏权限控制 ====================
-        var _currentUserRole = 0;
-        var _lastConnectionsData = null;
-        function loadUserInfo() {
-            return fetch('/api/auth/status')
-                .then(r => r.json())
-                .then(data => {
-                    const area = document.getElementById('user-info-area');
-                    if (data.authenticated) {
-                        _currentUserRole = data.role;
-                        const roles = {0: '普通用户', 1: '管理员', 2: '超级管理员'};
-                        const roleName = roles[data.role] || '未知角色';
-                        const roleClass = data.role === 2 ? 'style="background:rgba(255,215,0,0.3);color:#ffd700;"' : 
-                                          data.role === 1 ? 'style="background:rgba(64,169,255,0.3);color:#40a9ff;"' : '';
-                        area.innerHTML = '<span class="user-info">👤 ' + data.username + '</span>' +
-                            '<span class="role-badge" ' + roleClass + '>' + roleName + '</span>' +
-                            ' <button class="logout-btn" onclick="logout()">退出</button>';
-
-                        if (data.role >= 1) {
-                            show('nav-config');
-                        }
-                        if (data.role >= 2) {
-                            show('nav-system');
-                            show('nav-frpc');
-                            show('nav-ssh');
-                            show('nav-logs');
-                            show('nav-help');
-                        }
-                        if (data.role >= 2 && _lastConnectionsData) {
-                            renderConnectionsFrpc(_lastConnectionsData);
-                        }
-                    } else {
-                        area.innerHTML = '<a href="/login" style="color:white;text-decoration:underline;">登录</a>';
-                    }
-                })
-                .catch(e => console.error('用户信息加载失败:', e));
-        }
-
-        function show(id) {
-            var el = document.getElementById(id);
-            if (el) el.style.display = '';
-        }
-
-        function logout() {
-            fetch('/api/auth/logout', {method: 'POST', headers: {'X-CSRF-Token': getCookie('csrf_token')}})
-                .then(() => { window.location.href = '/login'; })
-                .catch(() => { window.location.href = '/login'; });
-        }
-
-        // ==================== 连接状态显示 ====================
-        function renderConnectionsFrpc(data) {
-            if (!data.frpc || _currentUserRole < 2) return;
-            var existing = document.getElementById('conn-frpc-item');
-            var dot = data.frpc.running ? 'online' : 'offline';
-            var detail = data.frpc.running ? (data.frpc.tunnels + ' 条隧道活跃') : '未运行';
-            var frpcHtml = '<div class="conn-item" id="conn-frpc-item"><div class="conn-dot ' + dot + '"></div>' +
-                '<div><div class="conn-label">🔗 frpc 隧道</div>' +
-                '<div class="conn-detail">' + detail + (data.frpc.version ? ' v' + data.frpc.version : '') + '</div></div></div>';
-            if (existing) {
-                existing.outerHTML = frpcHtml;
-            } else {
-                document.getElementById('connections-grid').insertAdjacentHTML('beforeend', frpcHtml);
-            }
-        }
-
-        function loadConnections() {
-            fetch('/api/connections')
-                .then(r => r.json())
-                .then(data => {
-                    _lastConnectionsData = data;
-                    let html = '';
-
-                    if (data.rtsp) {
-                        data.rtsp.forEach(cam => {
-                            let dot = 'disabled';
-                            let detail = '未配置';
-                            if (cam.configured && cam.running) { dot = 'online'; detail = '采集中'; }
-                            else if (cam.configured && !cam.running) { dot = 'offline'; detail = '已断开'; }
-                            else { dot = 'disabled'; detail = '未配置'; }
-                            html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                                '<div><div class="conn-label">📷 ' + cam.name + '</div>' +
-                                '<div class="conn-detail">' + detail + '</div></div></div>';
-                        });
-                    }
-
-                    if (data.mqtt) {
-                        let dot = 'disabled', detail = '未启用';
-                        if (data.mqtt.enabled) {
-                            if (data.mqtt.status === 1) { dot = 'online'; detail = data.mqtt.state + ' (' + data.mqtt.host + ')'; }
-                            else if (data.mqtt.status === 2) { dot = 'warn'; detail = data.mqtt.state + '...'; }
-                            else { dot = 'offline'; detail = data.mqtt.state; }
-                        }
-                        html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                            '<div><div class="conn-label">📨 MQTT</div>' +
-                            '<div class="conn-detail">' + detail + '</div></div></div>';
-                    }
-
-                    if (data.database) {
-                        let dot = data.database.connected ? 'online' : 'offline';
-                        let detail = data.database.connected ? 'SQLite 已连接' : '未连接';
-                        html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
-                            '<div><div class="conn-label">🗄️ 数据库</div>' +
-                            '<div class="conn-detail">' + detail + '</div></div></div>';
-                    }
-
-                    if (data.frpc && _currentUserRole >= 2) {
-                        html += '<div class="conn-item" id="conn-frpc-item"><div class="conn-dot ' + (data.frpc.running ? 'online' : 'offline') + '"></div>' +
-                            '<div><div class="conn-label">🔗 frpc 隧道</div>' +
-                            '<div class="conn-detail">' + (data.frpc.running ? data.frpc.tunnels + ' 条隧道活跃' : '未运行') + (data.frpc.version ? ' v' + data.frpc.version : '') + '</div></div></div>';
-                    }
-
-                    if (data.web) {
-                        let detail = '端口 ' + data.web.port + (data.web.https ? ' (HTTPS)' : '');
-                        html += '<div class="conn-item"><div class="conn-dot online"></div>' +
-                            '<div><div class="conn-label">🌐 Web 服务</div>' +
-                            '<div class="conn-detail">' + detail + '</div></div></div>';
-                    }
-
-                    document.getElementById('connections-grid').innerHTML = html;
-                })
-                .catch(e => {
-                    console.error('连接状态加载失败:', e);
-                    document.getElementById('connections-grid').innerHTML = 
-                        '<div class="conn-item"><div class="conn-dot offline"></div><div><div class="conn-label">加载失败</div></div></div>';
-                });
-        }
-
-        loadUserInfo();
-        loadConnections();
-        loadMonitorReport();
-        loadRealtimeData();
-        
-        // 自动刷新:只刷新实时数据 Tab
-        setInterval(() => {
-            if (currentTab === 'realtime') {
-                loadMonitorReport();
-                loadRealtimeData();
-            }
-        }, 30000);
-        
-        // 筛选框回车触发查询
-        ['filter-plate','filter-tbnum','filter-date-from','filter-date-to'].forEach(id => {
-            const el = document.getElementById(id);
-            if (el) el.addEventListener('keypress', e => { if (e.key === 'Enter') searchHistory(1); });
-        });
-    </script>
-</body>
-</html>

+ 0 - 284
assets/web/indexbak1.html

@@ -1,284 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v26</p>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 3 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;">' + rec.tb_num.substring(0, 15) + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                if (data.success) {
-                    alert('重试请求已提交');
-                    loadMessages(currentPage, currentKeyword);
-                } else {
-                    alert('重试失败');
-                }
-            })
-            .catch(e => alert('重试请求失败'));
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 310
assets/web/indexbak2.html

@@ -1,310 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix9</p>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;">' + rec.tb_num.substring(0, 22) + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 317
assets/web/indexbak3.html

@@ -1,317 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>车牌识别监控系统</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
-        .report-panel { width: 320px; flex-shrink: 0; }
-        .list-panel { flex: 1; min-width: 0; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
-        .metric:last-child { border-bottom: none; }
-        .metric-label { color: #666; font-size: 14px; }
-        .metric-value { font-weight: bold; color: #333; font-size: 14px; }
-        .metric-value.success { color: #52c41a; }
-        .metric-value.warning { color: #faad14; }
-        .metric-value.error { color: #f5222d; }
-        .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
-        .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
-        .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
-        .search-box button:hover { background: #764ba2; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-retry { background: #fff7e6; color: #fa8c16; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
-        .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
-        .photo-modal.active { display: flex; }
-        .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
-        .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
-        .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
-        .pagination button:hover { background: #f0f5ff; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination .info { color: #666; font-size: 14px; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        @media (max-width: 900px) {
-            .container { flex-direction: column; }
-            .report-panel { width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>车牌识别监控系统</h1>
-        <p>Plate Recognition Monitoring System v43.2-fix19</p>
-        <style>.video-link:hover{background:rgba(255,255,255,0.35)!important;}</style>
-        <div class="video-links" style="margin-top:12px;display:flex;justify-content:center;gap:10px;flex-wrap:wrap;">
-            <a href="/video?stream=in-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位进站</a>
-            <a href="/video?stream=in-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位进站</a>
-            <a href="/video?stream=out-low" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 低位出站</a>
-            <a href="/video?stream=out-high" class="video-link" style="background:rgba(255,255,255,0.2);color:white;padding:8px 16px;border-radius:20px;text-decoration:none;font-size:14px;transition:background 0.2s;">🎥 高位出站</a>
-        </div>
-    </div>
-    
-    <div class="container">
-        <div class="panel report-panel">
-            <h2>监控报告</h2>
-            <div id="monitor-report">
-                <div class="loading">加载中...</div>
-            </div>
-        </div>
-        
-        <div class="panel list-panel">
-            <h2>实时消息列表</h2>
-            <div class="search-box">
-                <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
-                <button onclick="searchMessages(1)">搜索</button>
-            </div>
-            <div class="table-container">
-                <table>
-                    <thead>
-                        <tr>
-                            <th>编号</th>
-                            <th>日期时间</th>
-                            <th>车牌号</th>
-                            <th>联单编号</th>
-                            <th>类型</th>
-                            <th>低位照片</th>
-                            <th>高位照片</th>
-                            <th>上传状态</th>
-                            <th>飞书状态</th>
-                            <th>操作</th>
-                        </tr>
-                    </thead>
-                    <tbody id="messages-body">
-                        <tr><td colspan="10" class="loading">加载中...</td></tr>
-                    </tbody>
-                </table>
-            </div>
-            <div class="pagination" id="pagination"></div>
-            <div class="refresh-info">数据自动刷新间隔: 30秒</div>
-        </div>
-    </div>
-    
-    <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
-        <span class="photo-modal-close">&times;</span>
-        <img id="photo-modal-img" src="" alt="照片">
-    </div>
-    
-    <script>
-        let currentPage = 1;
-        let currentKeyword = '';
-        
-        function loadMonitorReport() {
-            fetch('/api/monitor')
-                .then(r => r.json())
-                .then(data => {
-                    document.getElementById('monitor-report').innerHTML = `
-                        <div class="metric">
-                            <span class="metric-label">createBill成功率</span>
-                            <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">createBill总调用</span>
-                            <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">照片平均耗时</span>
-                            <span class="metric-value">${data.avg_upload_time}ms</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">TIME_WINDOW命中</span>
-                            <span class="metric-value">${data.time_window_hits}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站缓存命中率</span>
-                            <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站缓存命中率</span>
-                            <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">进站统计</span>
-                            <span class="metric-value">${data.in_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">出站统计</span>
-                            <span class="metric-value">${data.out_station_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总记录数</span>
-                            <span class="metric-value">${data.total_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">失败记录</span>
-                            <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">飞书成功数</span>
-                            <span class="metric-value">${data.feishu_success_count}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">系统运行时间</span>
-                            <span class="metric-value">${data.uptime}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">网络超时</span>
-                            <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">服务器错误</span>
-                            <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">总上传错误</span>
-                            <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定</span>
-                            <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">紧急告警抑制</span>
-                            <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
-                        </div>
-                        <div class="metric">
-                            <span class="metric-label">交替锁定间隔</span>
-                            <span class="metric-value">${data.in_out_interval_sec}秒</span>
-                        </div>
-                    `;
-                })
-                .catch(e => console.error('监控报告加载失败:', e));
-        }
-        
-        function loadMessages(page, keyword) {
-            let url = '/api/messages?page=' + page;
-            if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
-            
-            fetch(url)
-                .then(r => r.json())
-                .then(data => {
-                    let html = '';
-                    if (data.records.length === 0) {
-                        html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-                    } else {
-                        data.records.forEach(rec => {
-                            const stationType = rec.station_type === 1 ? '进站' : '出站';
-                            const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
-                            const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
-                            const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
-                            const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
-                            const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
-                            
-                            html += '<tr>';
-                            html += '<td>' + rec.id + '</td>';
-                            html += '<td>' + rec.create_time + '</td>';
-                            html += '<td><b>' + rec.plate_number + '</b></td>';
-                            html += '<td style="font-family:monospace;">' + rec.tb_num.substring(0, 15) + '</td>';
-                            html += '<td>' + stationType + '</td>';
-                            html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
-                            html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
-                            html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
-                            html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
-                            html += '</tr>';
-                        });
-                    }
-                    document.getElementById('messages-body').innerHTML = html;
-                    
-                    // 分页
-                    let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
-                    pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
-                    pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
-                    document.getElementById('pagination').innerHTML = pageHtml;
-                })
-                .catch(e => {
-                    console.error('消息列表加载失败:', e);
-                    document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-        
-        function searchMessages(page) {
-            currentPage = page;
-            currentKeyword = document.getElementById('search-input').value;
-            loadMessages(currentPage, currentKeyword);
-        }
-        
-        function retryRecord(id) {
-            if (!confirm('确定要重试此记录吗?')) return;
-            
-            var btn = event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-            
-            fetch('/api/retry', {
-                method: 'POST',
-                body: 'id=' + id,
-                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
-            })
-            .then(r => r.json())
-            .then(data => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    var msg = '✅ 重试成功';
-                    if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
-                    if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
-                    alert(msg);
-                } else {
-                    alert('❌ 重试失败: ' + (data.message || '未知错误'));
-                }
-                loadMessages(currentPage, currentKeyword);
-                loadMonitorReport();
-            })
-            .catch(e => {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                alert('重试请求失败: ' + e);
-            });
-        }
-        
-        function showPhoto(src) {
-            document.getElementById('photo-modal-img').src = src;
-            document.getElementById('photo-modal').classList.add('active');
-        }
-        
-        function closePhotoModal() {
-            document.getElementById('photo-modal').classList.remove('active');
-        }
-        
-        // 初始化
-        loadMonitorReport();
-        loadMessages(1, '');
-        
-        // 自动刷新
-        setInterval(() => {
-            loadMonitorReport();
-            if (currentPage === 1 && !currentKeyword) {
-                loadMessages(1, '');
-            }
-        }, 30000);
-        
-        // 回车搜索
-        document.getElementById('search-input').addEventListener('keypress', e => {
-            if (e.key === 'Enter') searchMessages(1);
-        });
-    </script>
-</body>
-</html>

+ 0 - 264
assets/web/login.html

@@ -1,264 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>PlateRecApp - 登录</title>
-    <style>
-        * { margin: 0; padding: 0; box-sizing: border-box; }
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: #0f1419;
-            color: #e1e8ed;
-            min-height: 100vh;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-        }
-        .login-container {
-            width: 100%;
-            max-width: 400px;
-            padding: 20px;
-        }
-        .login-card {
-            background: #1a2332;
-            border-radius: 12px;
-            padding: 40px 32px;
-            box-shadow: 0 8px 32px rgba(0,0,0,0.4);
-            border: 1px solid #2a3a4a;
-        }
-        .logo {
-            text-align: center;
-            margin-bottom: 32px;
-        }
-        .logo h1 {
-            font-size: 24px;
-            font-weight: 600;
-            color: #4fc3f7;
-            margin-bottom: 4px;
-        }
-        .logo p {
-            font-size: 13px;
-            color: #6b7b8d;
-        }
-        .form-group {
-            margin-bottom: 20px;
-        }
-        .form-group label {
-            display: block;
-            font-size: 13px;
-            color: #8899aa;
-            margin-bottom: 6px;
-            font-weight: 500;
-        }
-        .form-group input {
-            width: 100%;
-            padding: 12px 16px;
-            background: #0f1419;
-            border: 1px solid #2a3a4a;
-            border-radius: 8px;
-            color: #e1e8ed;
-            font-size: 15px;
-            transition: border-color 0.2s, box-shadow 0.2s;
-            outline: none;
-        }
-        .form-group input:focus {
-            border-color: #4fc3f7;
-            box-shadow: 0 0 0 3px rgba(79,195,247,0.15);
-        }
-        .form-group input::placeholder {
-            color: #4a5568;
-        }
-        .form-options {
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            margin-bottom: 24px;
-            font-size: 13px;
-        }
-        .remember-me {
-            display: flex;
-            align-items: center;
-            gap: 6px;
-            cursor: pointer;
-            color: #8899aa;
-        }
-        .remember-me input[type="checkbox"] {
-            width: 16px;
-            height: 16px;
-            accent-color: #4fc3f7;
-            cursor: pointer;
-        }
-        .btn-login {
-            width: 100%;
-            padding: 12px;
-            background: linear-gradient(135deg, #4fc3f7, #0288d1);
-            border: none;
-            border-radius: 8px;
-            color: white;
-            font-size: 15px;
-            font-weight: 600;
-            cursor: pointer;
-            transition: opacity 0.2s, transform 0.1s;
-        }
-        .btn-login:hover { opacity: 0.9; }
-        .btn-login:active { transform: scale(0.98); }
-        .btn-login:disabled {
-            opacity: 0.6;
-            cursor: not-allowed;
-        }
-        .error-msg {
-            background: rgba(239,68,68,0.1);
-            border: 1px solid rgba(239,68,68,0.3);
-            color: #f87171;
-            padding: 10px 14px;
-            border-radius: 8px;
-            font-size: 13px;
-            margin-bottom: 16px;
-            display: none;
-        }
-        .error-msg.show { display: block; }
-        .rate-limit-msg {
-            background: rgba(245,158,11,0.1);
-            border: 1px solid rgba(245,158,11,0.3);
-            color: #fbbf24;
-            padding: 10px 14px;
-            border-radius: 8px;
-            font-size: 13px;
-            margin-bottom: 16px;
-            display: none;
-        }
-        .footer {
-            text-align: center;
-            margin-top: 24px;
-            font-size: 12px;
-            color: #4a5568;
-        }
-        .loading-spinner {
-            display: inline-block;
-            width: 16px;
-            height: 16px;
-            border: 2px solid rgba(255,255,255,0.3);
-            border-top-color: white;
-            border-radius: 50%;
-            animation: spin 0.6s linear infinite;
-            margin-right: 8px;
-            vertical-align: middle;
-        }
-        @keyframes spin { to { transform: rotate(360deg); } }
-    
-        @media (max-width: 480px) {
-            .login-container { padding: 12px; }
-            .login-card { padding: 24px 20px; }
-            .logo h1 { font-size: 20px; }
-            .form-group input { padding: 10px 12px; font-size: 14px; }
-            .btn-login { padding: 10px; font-size: 14px; }
-        }
-    </style>
-</head>
-<body>
-    <div class="login-container">
-        <div class="login-card">
-            <div class="logo">
-                <h1>🔒 PlateRecApp</h1>
-                <p>车牌识别管理系统</p>
-            </div>
-
-            <div id="error-msg" class="error-msg"></div>
-            <div id="rate-limit-msg" class="rate-limit-msg"></div>
-
-            <form id="login-form" autocomplete="off">
-                <div class="form-group">
-                    <label for="username">用户名</label>
-                    <input type="text" id="username" name="username"
-                           placeholder="请输入用户名" required autofocus
-                           autocomplete="username">
-                </div>
-                <div class="form-group">
-                    <label for="password">密码</label>
-                    <input type="password" id="password" name="password"
-                           placeholder="请输入密码" required
-                           autocomplete="current-password">
-                </div>
-                <div class="form-options">
-                    <label class="remember-me">
-                        <input type="checkbox" id="remember" name="remember">
-                        记住登录 (7天)
-                    </label>
-                </div>
-                <button type="submit" class="btn-login" id="btn-login">登 录</button>
-            </form>
-
-            <div class="footer">
-                PlateRecApp v5.0 | Secure Login
-            </div>
-        </div>
-    </div>
-
-    <script>
-        const form = document.getElementById('login-form');
-        const btnLogin = document.getElementById('btn-login');
-        const errorDiv = document.getElementById('error-msg');
-        const rateLimitDiv = document.getElementById('rate-limit-msg');
-
-        // Check if already logged in
-        fetch('/api/auth/status')
-            .then(r => r.json())
-            .then(data => {
-                if (data.authenticated) {
-                    const redirect = new URLSearchParams(window.location.search).get('redirect') || '/';
-                    window.location.href = redirect;
-                }
-            })
-            .catch(() => {});
-
-        form.addEventListener('submit', async (e) => {
-            e.preventDefault();
-            errorDiv.classList.remove('show');
-            rateLimitDiv.classList.remove('show');
-            btnLogin.disabled = true;
-            btnLogin.innerHTML = '<span class="loading-spinner"></span>登录中...';
-
-            const username = document.getElementById('username').value.trim();
-            const password = document.getElementById('password').value;
-            const remember = document.getElementById('remember').checked;
-
-            try {
-                const resp = await fetch('/api/auth/login', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ username, password, remember })
-                });
-
-                const data = await resp.json();
-
-                if (resp.status === 429) {
-                    rateLimitDiv.textContent = data.message || '请求过于频繁,请稍后再试';
-                    rateLimitDiv.classList.add('show');
-                } else if (resp.status === 401) {
-                    errorDiv.textContent = data.message || '用户名或密码错误';
-                    errorDiv.classList.add('show');
-                    document.getElementById('password').value = '';
-                    document.getElementById('password').focus();
-                } else if (resp.status === 423) {
-                    errorDiv.textContent = data.message || '账户已锁定,请稍后再试';
-                    errorDiv.classList.add('show');
-                } else if (data.success) {
-                    const redirect = new URLSearchParams(window.location.search).get('redirect') || '/';
-                    window.location.href = redirect;
-                    return;
-                } else {
-                    errorDiv.textContent = data.message || '登录失败';
-                    errorDiv.classList.add('show');
-                }
-            } catch (err) {
-                errorDiv.textContent = '网络错误,请重试';
-                errorDiv.classList.add('show');
-            }
-
-            btnLogin.disabled = false;
-            btnLogin.textContent = '登 录';
-        });
-    </script>
-</body>
-</html>

+ 0 - 167
assets/web/logs.html

@@ -1,167 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>日志管理 - PlateRecApp</title>
-<style>
-*{margin:0;padding:0;box-sizing:border-box}
-body{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
-.header{background:#1e293b;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155}
-.header h1{font-size:20px;font-weight:600;color:#f8fafc}
-.nav{display:flex;gap:16px;align-items:center}
-.nav a{color:#60a5fa;text-decoration:none;font-size:14px;padding:6px 12px;border-radius:6px}
-.nav a:hover,.nav a.active{background:#334155}
-.container{max-width:1200px;margin:0 auto;padding:20px}
-.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px}
-.card{background:#1e293b;border-radius:12px;padding:20px;border:1px solid #334155}
-.card-title{font-size:13px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
-.card-value{font-size:28px;font-weight:700}
-.card-sub{font-size:13px;color:#64748b;margin-top:4px}
-.log-card{background:#1e293b;border-radius:12px;padding:20px;border:1px solid #334155;margin-bottom:16px}
-.log-card-title{font-size:15px;font-weight:600;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center}
-pre{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:12px;font-family:'Fira Code',monospace;font-size:12px;line-height:1.6;max-height:500px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;color:#cbd5e1}
-.btn{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s}
-.btn-primary{background:#3b82f6;color:#fff}.btn-primary:hover{background:#2563eb}
-.btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c}
-.btn-warning{background:#d97706;color:#fff}.btn-warning:hover{background:#b45309}
-.btn-group{display:flex;gap:8px;flex-wrap:wrap}
-.progress-bar{height:8px;background:#334155;border-radius:4px;margin-top:8px;overflow:hidden}
-.progress-fill{height:100%;border-radius:4px;transition:width .5s}
-.msg{padding:10px 14px;border-radius:8px;margin-top:10px;font-size:14px;display:none}
-.msg.success{display:block;background:#14532d;color:#86efac;border:1px solid #16a34a}
-.msg.error{display:block;background:#7f1d1d;color:#fca5a5;border:1px solid #dc2626}
-
-    @media (max-width: 768px) {
-        .header { flex-direction: column; gap: 8px; padding: 12px 16px; }
-        .header h1 { font-size: 16px; }
-        .nav { flex-wrap: wrap; gap: 8px; justify-content: center; }
-        .nav a { font-size: 12px; padding: 4px 8px; }
-        .container { padding: 12px; }
-        .grid { grid-template-columns: 1fr 1fr; gap: 10px; }
-        .card { padding: 14px; }
-        .card-value { font-size: 22px; }
-        .log-card { padding: 14px; }
-        .log-card-title { font-size: 13px; flex-direction: column; gap: 8px; align-items: flex-start; }
-        pre { font-size: 11px; max-height: 300px; padding: 10px; }
-        .btn-group { flex-wrap: wrap; }
-        .btn { flex: 1; min-width: 80px; text-align: center; font-size: 12px; }
-    }
-    @media (max-width: 480px) {
-        .grid { grid-template-columns: 1fr; }
-        .card-value { font-size: 20px; }
-    }
-    </style>
-</head>
-<body>
-<div class="header">
-    <h1>📋 日志管理</h1>
-    <div class="nav">
-        <a href="/">监控台</a>
-        <a href="/video">视频</a>
-        <a href="/config">配置</a>
-        <a href="/system">系统</a>
-        <a href="/frpc">frpc</a>
-        <a href="/ssh-keys">SSH</a>
-        <a href="/logs" class="active">日志</a>
-    </div>
-</div>
-<div class="container">
-    <div class="grid">
-        <div class="card">
-            <div class="card-title">文件大小</div>
-            <div class="card-value" id="file-size">-- MB</div>
-            <div class="card-sub">缓冲区上限: <span id="buffer-max">-</span> MB</div>
-            <div class="progress-bar"><div class="progress-fill" id="buffer-bar" style="width:0%"></div></div>
-        </div>
-        <div class="card">
-            <div class="card-title">内存缓冲</div>
-            <div class="card-value" id="memory-usage">-- MB</div>
-            <div class="card-sub">模式: <span id="log-mode">-</span> | 条目: <span id="line-count">-</span></div>
-            <div class="progress-bar"><div class="progress-fill" id="memory-bar" style="width:0%"></div></div>
-        </div>
-        <div class="card">
-            <div class="card-title">定时刷盘</div>
-            <div class="card-value" id="flush-time">--</div>
-            <div class="card-sub">上次刷盘: <span id="last-flush">-</span> | 保留: <span id="retention">-</span> 天</div>
-        </div>
-        <div class="card">
-            <div class="card-title">刷盘统计</div>
-            <div class="card-value" id="flush-count">--</div>
-            <div class="card-sub">磁盘文件: <span id="last-modified">-</span></div>
-        </div>
-    </div>
-    <div class="btn-group" style="margin-bottom:16px">
-        <button class="btn btn-primary" onclick="refreshStats()">🔄 刷新状态</button>
-        <button class="btn btn-warning" onclick="flushLog()">⚡ 手动刷新日志</button>
-        <button class="btn btn-danger" onclick="cleanupLogs()">🗑️ 清理过期日志</button>
-        <button class="btn btn-primary" onclick="refreshTail()">📄 刷新日志尾部</button>
-    </div>
-    <div id="msg" class="msg"></div>
-    <div class="log-card">
-        <div class="log-card-title">
-            <span>日志尾部 (最后 200 行)</span>
-            <span style="font-size:13px;color:#64748b" id="log-path">-</span>
-        </div>
-        <pre id="log-content">加载中...</pre>
-    </div>
-</div>
-<script>
-function getCookie(n){var m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}function authFetch(u,o){o=o||{};var h=o.headers||{};if(!(h instanceof Headers)){var nh=new Headers();for(var k in h)nh.set(k,h[k]);h=nh;}if(!h.has("X-CSRF-Token")){var t=getCookie("csrf_token");if(t)h.set("X-CSRF-Token",t);}o.headers=h;return fetch(u,o).then(function(r){if(r.status===401){window.location.href="/login?redirect="+encodeURIComponent(window.location.pathname);return new Promise(function(){});}return r;});}
-function showMsg(text,ok){
-    let el=document.getElementById('msg');
-    el.className='msg '+(ok?'success':'error');
-    el.textContent=text;
-    setTimeout(()=>el.className='msg',5000);
-}
-function refreshStats(){
-    authFetch('/api/logs/stats').then(r=>r.json()).then(d=>{
-        document.getElementById('file-size').textContent=d.size_mb+' MB';
-        document.getElementById('buffer-max').textContent=d.buffer_mb;
-        document.getElementById('line-count').textContent=d.line_count.toLocaleString();
-        document.getElementById('last-modified').textContent=d.last_modified||'-';
-        document.getElementById('flush-time').textContent=d.flush_time;
-        document.getElementById('retention').textContent=d.retention_days;
-        document.getElementById('log-path').textContent=d.file;
-        // ✅ fix24-v36: 内存缓冲新增字段
-        document.getElementById('log-mode').textContent=d.memory_enabled?'内存模式':'直写模式';
-        document.getElementById('memory-usage').textContent=(d.memory_usage_mb||0).toFixed(2)+' MB';
-        document.getElementById('last-flush').textContent=d.last_flush_time||'未刷盘';
-        document.getElementById('flush-count').textContent=(d.total_flush_count||0)+' 次';
-        // 磁盘文件进度条
-        let bar=document.getElementById('buffer-bar');
-        bar.style.width=Math.min(d.buffer_percent,100)+'%';
-        if(d.buffer_percent<70) bar.style.background='#4ade80';
-        else if(d.buffer_percent<90) bar.style.background='#facc15';
-        else bar.style.background='#f87171';
-        // 内存缓冲进度条
-        let mbar=document.getElementById('memory-bar');
-        let memPercent=d.buffer_percent||0;
-        mbar.style.width=Math.min(memPercent,100)+'%';
-        if(memPercent<70) mbar.style.background='#4ade80';
-        else if(memPercent<90) mbar.style.background='#facc15';
-        else mbar.style.background='#f87171';
-    });
-}
-function refreshTail(){
-    document.getElementById('log-content').textContent='加载中...';
-    authFetch('/api/logs/tail?lines=200').then(r=>r.text()).then(t=>{
-        document.getElementById('log-content').textContent=t||'(空日志)';
-    });
-}
-function flushLog(){
-    authFetch('/api/logs/flush',{method:'POST'}).then(r=>r.json()).then(d=>{
-        showMsg(d.message||d.error,d.success);
-    });
-}
-function cleanupLogs(){
-    if(!confirm('确定要清理过期日志文件吗?')) return;
-    authFetch('/api/logs/cleanup',{method:'POST'}).then(r=>r.json()).then(d=>{
-        showMsg(d.message||d.error,d.success);
-    });
-}
-refreshStats();
-refreshTail();
-</script>
-</body>
-</html>

+ 0 - 129
assets/web/ssh_keys.html

@@ -1,129 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>SSH 密钥管理 - PlateRecApp</title>
-<style>
-*{margin:0;padding:0;box-sizing:border-box}
-body{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
-.header{background:#1e293b;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155}
-.header h1{font-size:20px;font-weight:600;color:#f8fafc}
-.nav{display:flex;gap:16px;align-items:center}
-.nav a{color:#60a5fa;text-decoration:none;font-size:14px;padding:6px 12px;border-radius:6px}
-.nav a:hover,.nav a.active{background:#334155}
-.container{max-width:900px;margin:0 auto;padding:20px}
-.card{background:#1e293b;border-radius:12px;padding:20px;border:1px solid #334155;margin-bottom:16px}
-.card-title{font-size:15px;font-weight:600;margin-bottom:12px;color:#f8fafc}
-table{width:100%;border-collapse:collapse;font-size:14px}
-th{text-align:left;padding:8px 12px;color:#94a3b8;border-bottom:1px solid #334155;font-weight:500}
-td{padding:8px 12px;border-bottom:1px solid #1e293b}
-.key-type{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500}
-.key-type.rsa{background:#854d0e;color:#fde047}
-.key-type.ed25519{background:#1e3a5f;color:#7dd3fc}
-.key-type.other{background:#374151;color:#9ca3af}
-.btn{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:14px;font-weight:500;transition:all .2s}
-.btn-primary{background:#3b82f6;color:#fff}.btn-primary:hover{background:#2563eb}
-.btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c}
-.btn-sm{padding:4px 10px;font-size:12px}
-textarea{width:100%;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:8px;padding:12px;font-family:'Fira Code',monospace;font-size:13px;resize:vertical;min-height:80px}
-textarea:focus{outline:none;border-color:#3b82f6}
-.msg{padding:10px 14px;border-radius:8px;margin-top:10px;font-size:14px;display:none}
-.msg.success{display:block;background:#14532d;color:#86efac;border:1px solid #16a34a}
-.msg.error{display:block;background:#7f1d1d;color:#fca5a5;border:1px solid #dc2626}
-.empty{text-align:center;color:#64748b;padding:20px}
-
-    @media (max-width: 768px) {
-        .header { flex-direction: column; gap: 8px; padding: 12px 16px; }
-        .header h1 { font-size: 16px; }
-        .nav { flex-wrap: wrap; gap: 8px; justify-content: center; }
-        .nav a { font-size: 12px; padding: 4px 8px; }
-        .container { padding: 12px; }
-        .card { padding: 14px; }
-        table { font-size: 12px; display: block; overflow-x: auto; white-space: nowrap; }
-        th, td { padding: 6px 8px; }
-        .btn-group { flex-wrap: wrap; }
-        .btn { flex: 1; min-width: 80px; text-align: center; font-size: 12px; }
-        textarea { min-height: 60px; font-size: 12px; }
-    }
-    @media (max-width: 480px) {
-        .card-title { font-size: 13px; }
-    }
-    </style>
-</head>
-<body>
-<div class="header">
-    <h1>🔑 SSH 密钥管理</h1>
-    <div class="nav">
-        <a href="/">监控台</a>
-        <a href="/video">视频</a>
-        <a href="/config">配置</a>
-        <a href="/system">系统</a>
-        <a href="/frpc">frpc</a>
-        <a href="/ssh-keys" class="active">SSH</a>
-        <a href="/logs">日志</a>
-        <a href="/help">帮助</a>
-    </div>
-</div>
-<div class="container">
-    <div class="card">
-        <div class="card-title">已授权公钥</div>
-        <table>
-            <thead><tr><th>#</th><th>类型</th><th>注释</th><th>密钥预览</th><th>操作</th></tr></thead>
-            <tbody id="key-list"><tr><td colspan="5" class="empty">加载中...</td></tr></tbody>
-        </table>
-    </div>
-    <div class="card">
-        <div class="card-title">添加公钥</div>
-        <textarea id="new-key" placeholder="粘贴 SSH 公钥,例如:&#10;ssh-rsa AAAAB3NzaC1yc2EAAA... user@host&#10;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@host"></textarea>
-        <div style="margin-top:10px">
-            <button class="btn btn-primary" onclick="addKey()">➕ 添加公钥</button>
-            <button class="btn btn-primary" onclick="loadKeys()" style="margin-left:8px">🔄 刷新列表</button>
-        </div>
-        <div id="msg" class="msg"></div>
-    </div>
-</div>
-<script>
-function getCookie(n){var m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}function authFetch(u,o){o=o||{};var h=o.headers||{};if(!(h instanceof Headers)){var nh=new Headers();for(var k in h)nh.set(k,h[k]);h=nh;}if(!h.has("X-CSRF-Token")){var t=getCookie("csrf_token");if(t)h.set("X-CSRF-Token",t);}o.headers=h;return fetch(u,o).then(function(r){if(r.status===401){window.location.href="/login?redirect="+encodeURIComponent(window.location.pathname);return new Promise(function(){});}return r;});}
-function showMsg(text,ok){
-    let el=document.getElementById('msg');
-    el.className='msg '+(ok?'success':'error');
-    el.textContent=text;
-    setTimeout(()=>el.className='msg',5000);
-}
-function getKeyTypeClass(type){
-    if(type.includes('ed25519'))return'ed25519';
-    if(type.includes('rsa'))return'rsa';
-    return'other';
-}
-function loadKeys(){
-    authFetch('/api/ssh/keys').then(r=>r.json()).then(d=>{
-        let tbody=document.getElementById('key-list');
-        if(!d.keys||d.keys.length===0){
-            tbody.innerHTML='<tr><td colspan="5" class="empty">暂无授权公钥</td></tr>';
-            return;
-        }
-        tbody.innerHTML=d.keys.map((k,i)=>'<tr><td>'+(i+1)+'</td><td><span class="key-type '+getKeyTypeClass(k.type)+'">'+k.type+'</span></td><td>'+k.comment+'</td><td style="font-family:monospace;font-size:12px;color:#64748b">'+k.key_preview+'</td><td><button class="btn btn-danger btn-sm" onclick="deleteKey('+k.index+')">删除</button></td></tr>').join('');
-    });
-}
-function addKey(){
-    let key=document.getElementById('new-key').value.trim();
-    if(!key){showMsg('请输入公钥',false);return}
-    authFetch('/api/ssh/keys',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:key})})
-    .then(r=>r.json()).then(d=>{
-        showMsg(d.message||d.error,d.success);
-        if(d.success){document.getElementById('new-key').value='';loadKeys()}
-    });
-}
-function deleteKey(index){
-    if(!confirm('确定要删除第 '+(index+1)+' 条公钥吗?此操作不可撤销!')) return;
-    authFetch('/api/ssh/keys',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({index:index})})
-    .then(r=>r.json()).then(d=>{
-        showMsg(d.message||d.error,d.success);
-        if(d.success) loadKeys();
-    });
-}
-loadKeys();
-</script>
-</body>
-</html>

+ 0 - 174
assets/web/system.html

@@ -1,174 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>系统监控 - PlateRecApp</title>
-<style>
-*{margin:0;padding:0;box-sizing:border-box}
-body{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
-.header{background:#1e293b;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #334155}
-.header h1{font-size:20px;font-weight:600;color:#f8fafc}
-.header a{color:#60a5fa;text-decoration:none;font-size:14px}
-.nav{display:flex;gap:16px;align-items:center}
-.nav a{padding:6px 12px;border-radius:6px;transition:background .2s}
-.nav a:hover{background:#334155}
-.container{max-width:1200px;margin:0 auto;padding:20px}
-.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin-bottom:20px}
-.card{background:#1e293b;border-radius:12px;padding:20px;border:1px solid #334155}
-.card-title{font-size:13px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px}
-.card-value{font-size:32px;font-weight:700;margin-bottom:4px}
-.card-sub{font-size:13px;color:#64748b}
-.progress-bar{height:8px;background:#334155;border-radius:4px;margin-top:8px;overflow:hidden}
-.progress-fill{height:100%;border-radius:4px;transition:width .5s}
-.green{color:#4ade80}.yellow{color:#facc15}.red{color:#f87171}
-.bg-green{background:#4ade80}.bg-yellow{background:#facc15}.bg-red{background:#f87171}
-.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:14px}
-.info-grid .label{color:#94a3b8}.info-grid .val{color:#e2e8f0;text-align:right}
-.alert-banner{background:#7f1d1d;border:1px solid #dc2626;border-radius:8px;padding:12px 16px;margin-bottom:16px;display:none;color:#fca5a5;font-size:14px}
-.refresh-info{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;font-size:13px;color:#64748b}
-
-    @media (max-width: 768px) {
-        .header { flex-direction: column; gap: 8px; padding: 12px 16px; }
-        .header h1 { font-size: 16px; }
-        .nav { flex-wrap: wrap; gap: 8px; justify-content: center; }
-        .nav a { font-size: 12px; padding: 4px 8px; }
-        .container { padding: 12px; }
-        .grid { grid-template-columns: 1fr; gap: 12px; }
-        .card { padding: 14px; }
-        .card-value { font-size: 24px; }
-        .info-grid { grid-template-columns: 1fr; gap: 4px; }
-        .info-grid .val { text-align: left; }
-        .refresh-info { flex-direction: column; gap: 8px; text-align: center; }
-    }
-    @media (max-width: 480px) {
-        .card-value { font-size: 20px; }
-        .card-title { font-size: 11px; }
-    }
-    </style>
-</head>
-<body>
-<div class="header">
-    <h1>🖥️ 系统监控</h1>
-    <div class="nav">
-        <a href="/">监控台</a>
-        <a href="/video">视频</a>
-        <a href="/config">配置</a>
-        <a href="/system" style="background:#334155">系统</a>
-        <a href="/frpc">frpc</a>
-        <a href="/ssh-keys">SSH</a>
-        <a href="/logs">日志</a>
-        <a href="/help">帮助</a>
-    </div>
-</div>
-<div class="container">
-    <div id="alert-banner" class="alert-banner"></div>
-    <div class="refresh-info">
-        <span>自动刷新: 每 <span id="interval">2</span> 秒</span>
-        <span>最后更新: <span id="last-update">--</span></span>
-    </div>
-    <div class="grid">
-        <div class="card">
-            <div class="card-title">CPU 使用率</div>
-            <div class="card-value" id="cpu-usage">--%</div>
-            <div class="card-sub"><span id="cpu-cores">-</span> 核 | 负载: <span id="cpu-load">-</span></div>
-            <div class="progress-bar"><div class="progress-fill bg-green" id="cpu-bar" style="width:0%"></div></div>
-        </div>
-        <div class="card">
-            <div class="card-title">内存使用</div>
-            <div class="card-value" id="mem-usage">--%</div>
-            <div class="card-sub"><span id="mem-detail">-</span></div>
-            <div class="progress-bar"><div class="progress-fill bg-green" id="mem-bar" style="width:0%"></div></div>
-        </div>
-        <div class="card">
-            <div class="card-title">CPU 温度</div>
-            <div class="card-value" id="cpu-temp">--°C</div>
-            <div class="card-sub">告警阈值: <span id="temp-threshold">-</span>°C</div>
-            <div class="progress-bar"><div class="progress-fill bg-green" id="temp-bar" style="width:0%"></div></div>
-        </div>
-        <div class="card">
-            <div class="card-title">磁盘使用</div>
-            <div class="card-value" id="disk-usage">--%</div>
-            <div class="card-sub"><span id="disk-detail">-</span></div>
-            <div class="progress-bar"><div class="progress-fill bg-green" id="disk-bar" style="width:0%"></div></div>
-        </div>
-    </div>
-    <div class="grid">
-        <div class="card">
-            <div class="card-title">系统信息</div>
-            <div class="info-grid">
-                <span class="label">主机名</span><span class="val" id="hostname">-</span>
-                <span class="label">内核版本</span><span class="val" id="kernel">-</span>
-                <span class="label">运行时间</span><span class="val" id="uptime">-</span>
-                <span class="label">进程PID</span><span class="val" id="pid">-</span>
-                <span class="label">进程内存</span><span class="val" id="proc-rss">-</span>
-            </div>
-        </div>
-        <div class="card">
-            <div class="card-title">告警阈值</div>
-            <div class="info-grid">
-                <span class="label">CPU 告警</span><span class="val" id="cpu-threshold">-</span>
-                <span class="label">温度告警</span><span class="val" id="temp-threshold2">-</span>
-            </div>
-        </div>
-    </div>
-</div>
-<script>
-function getCookie(n){var m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}function authFetch(u,o){o=o||{};var h=o.headers||{};if(!(h instanceof Headers)){var nh=new Headers();for(var k in h)nh.set(k,h[k]);h=nh;}if(!h.has("X-CSRF-Token")){var t=getCookie("csrf_token");if(t)h.set("X-CSRF-Token",t);}o.headers=h;return fetch(u,o).then(function(r){if(r.status===401){window.location.href="/login?redirect="+encodeURIComponent(window.location.pathname);return new Promise(function(){});}return r;});}
-function getColor(pct){return pct<60?'green':pct<85?'yellow':'red'}
-function updateBar(id,pct,max){
-    let bar=document.getElementById(id);
-    bar.style.width=Math.min(pct/max*100,100)+'%';
-    bar.className='progress-fill bg-'+getColor(pct);
-}
-function update(){
-    authFetch('/api/system/status').then(r=>r.json()).then(d=>{
-        // CPU
-        document.getElementById('cpu-usage').textContent=d.cpu.usage.toFixed(1)+'%';
-        document.getElementById('cpu-usage').className='card-value '+getColor(d.cpu.usage);
-        document.getElementById('cpu-cores').textContent=d.cpu.cores;
-        document.getElementById('cpu-load').textContent=d.cpu.load1.toFixed(2)+' / '+d.cpu.load5.toFixed(2)+' / '+d.cpu.load15.toFixed(2);
-        updateBar('cpu-bar',d.cpu.usage,100);
-        // Memory
-        document.getElementById('mem-usage').textContent=d.memory.percent.toFixed(1)+'%';
-        document.getElementById('mem-usage').className='card-value '+getColor(d.memory.percent);
-        document.getElementById('mem-detail').textContent=d.memory.used+' MB / '+d.memory.total+' MB (可用 '+d.memory.available+' MB)';
-        updateBar('mem-bar',d.memory.percent,100);
-        // Temperature
-        if(d.temperature.available){
-            document.getElementById('cpu-temp').textContent=d.temperature.value.toFixed(1)+'°C';
-            let tc=d.temperature.value>d.temperature.alert_threshold?'red':d.temperature.value>60?'yellow':'green';
-            document.getElementById('cpu-temp').className='card-value '+tc;
-            updateBar('temp-bar',d.temperature.value,d.temperature.alert_threshold*1.3);
-        }else{
-            document.getElementById('cpu-temp').textContent='不可用';
-            document.getElementById('cpu-temp').className='card-value';
-        }
-        document.getElementById('temp-threshold').textContent=d.temperature.alert_threshold;
-        // Disk
-        document.getElementById('disk-usage').textContent=d.disk.percent.toFixed(1)+'%';
-        document.getElementById('disk-usage').className='card-value '+getColor(d.disk.percent);
-        document.getElementById('disk-detail').textContent=d.disk.used+' GB / '+d.disk.total+' GB (可用 '+d.disk.available+' GB)';
-        updateBar('disk-bar',d.disk.percent,100);
-        // System
-        document.getElementById('hostname').textContent=d.system.hostname;
-        document.getElementById('kernel').textContent=d.system.kernel;
-        document.getElementById('uptime').textContent=d.system.uptime;
-        document.getElementById('pid').textContent=d.process.pid;
-        document.getElementById('proc-rss').textContent=d.process.rss_mb+' MB';
-        document.getElementById('cpu-threshold').textContent=d.thresholds.cpu_alert+'%';
-        document.getElementById('temp-threshold2').textContent=d.thresholds.temp_alert+'°C';
-        // Alerts
-        let alerts=[];
-        if(d.cpu.usage>d.thresholds.cpu_alert) alerts.push('⚠️ CPU使用率 '+d.cpu.usage.toFixed(1)+'% 超过阈值 '+d.thresholds.cpu_alert+'%');
-        if(d.temperature.available&&d.temperature.value>d.temperature.alert_threshold) alerts.push('⚠️ CPU温度 '+d.temperature.value.toFixed(1)+'°C 超过阈值 '+d.temperature.alert_threshold+'°C');
-        let ab=document.getElementById('alert-banner');
-        if(alerts.length){ab.style.display='block';ab.innerHTML=alerts.join('<br>')}else{ab.style.display='none'}
-        document.getElementById('last-update').textContent=new Date().toLocaleTimeString();
-    }).catch(e=>console.error(e));
-}
-update();
-setInterval(update,2000);
-</script>
-</body>
-</html>

+ 0 - 381
assets/web/video.ba3.html

@@ -1,381 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>视频监控</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column;
-        }
-        .top-bar {
-            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-            padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
-            flex-shrink: 0;
-        }
-        .top-bar h1 { font-size: 18px; color: white; }
-        .nav-links { display: flex; gap: 8px; }
-        .nav-link {
-            background: rgba(255,255,255,0.2); color: white; padding: 6px 14px;
-            border-radius: 16px; text-decoration: none; font-size: 13px;
-            transition: background 0.2s; white-space: nowrap;
-        }
-        .nav-link:hover { background: rgba(255,255,255,0.35); }
-        .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
-
-        .video-area {
-            flex: 1; display: flex; flex-direction: column; align-items: center;
-            padding: 16px; overflow: auto;
-        }
-        .video-wrapper {
-            position: relative; background: #000; border-radius: 8px;
-            overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5);
-            max-width: 960px; width: 100%;
-        }
-        .video-wrapper img { width: 100%; display: block; }
-        #roi-canvas {
-            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
-            cursor: crosshair; display: none; z-index: 10;
-        }
-        .stream-label {
-            position: absolute; top: 8px; left: 12px; background: rgba(0,0,0,0.6);
-            color: white; padding: 4px 10px; border-radius: 4px; font-size: 13px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status {
-            position: absolute; top: 8px; right: 12px; background: rgba(0,0,0,0.6);
-            color: #52c41a; padding: 4px 10px; border-radius: 4px; font-size: 12px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status.offline { color: #f5222d; }
-
-        .controls {
-            margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap;
-            justify-content: center; max-width: 960px; width: 100%;
-        }
-        .btn {
-            padding: 10px 20px; border: 1px solid #444; background: #2a2a4a;
-            color: #eee; border-radius: 8px; cursor: pointer; font-size: 14px;
-            transition: all 0.2s; display: flex; align-items: center; gap: 6px;
-        }
-        .btn:hover { background: #3a3a5a; border-color: #667eea; }
-        .btn.primary { background: #667eea; border-color: #667eea; }
-        .btn.primary:hover { background: #5a6ed6; }
-        .btn.danger { background: #ff4d4f; border-color: #ff4d4f; color: white; }
-        .btn.danger:hover { background: #e04445; }
-        .btn.drawing { background: #ff4d4f; color: white; border-color: #ff4d4f; animation: pulse 1.5s infinite; }
-        .btn:disabled { opacity: 0.5; cursor: not-allowed; }
-
-        @keyframes pulse {
-            0%, 100% { opacity: 1; }
-            50% { opacity: 0.7; }
-        }
-
-        .roi-info {
-            margin-top: 12px; padding: 12px 16px; background: #2a2a4a;
-            border-radius: 8px; font-size: 13px; line-height: 1.8;
-            max-width: 960px; width: 100%;
-        }
-        .roi-info .label { color: #999; }
-        .roi-info .value { color: #667eea; font-weight: bold; }
-        .help-text { margin-top: 8px; font-size: 12px; color: #666; text-align: center; }
-
-        @media (max-width: 600px) {
-            .top-bar { flex-direction: column; gap: 8px; }
-            .nav-links { flex-wrap: wrap; justify-content: center; }
-        }
-    </style>
-</head>
-<body>
-    <div class="top-bar">
-        <h1 id="page-title">视频监控</h1>
-        <div class="nav-links">
-            <a href="/" class="nav-link">📊 监控台</a>
-            <a href="/video?stream=in-low" class="nav-link" id="nav-in-low">低位进站</a>
-            <a href="/video?stream=in-high" class="nav-link" id="nav-in-high">高位进站</a>
-            <a href="/video?stream=out-low" class="nav-link" id="nav-out-low">低位出站</a>
-            <a href="/video?stream=out-high" class="nav-link" id="nav-out-high">高位出站</a>
-        </div>
-    </div>
-
-    <div class="video-area">
-        <div class="video-wrapper" id="video-wrapper">
-            <div class="stream-label" id="stream-label">加载中...</div>
-            <div class="stream-status" id="stream-status">⏳ 连接中</div>
-            <img id="video-stream" src="" alt="视频流">
-            <canvas id="roi-canvas"></canvas>
-        </div>
-
-        <div class="controls">
-            <button class="btn primary" onclick="takeSnapshot()">📷 截图</button>
-            <button class="btn" id="btn-draw-roi" onclick="startDrawROI()">✏️ 绘制ROI</button>
-            <button class="btn" id="btn-clear-roi" onclick="clearROI()">🗑️ 清除ROI</button>
-            <button class="btn" onclick="refreshStream()">🔄 刷新视频</button>
-        </div>
-
-        <div class="roi-info" id="roi-info">
-            <span class="label">加载配置中...</span>
-        </div>
-        <div class="help-text" id="help-text"></div>
-    </div>
-
-    <script>
-    var STREAM_NAMES = {
-        'in-low': '低位进站', 'in-high': '高位进站',
-        'out-low': '低位出站', 'out-high': '高位出站'
-    };
-    var ROI_STREAMS = ['in-low', 'out-low'];
-    var currentStream = 'in-low';
-    var hasROI = false;
-    var isDrawingROI = false;
-    var drawStart = null;
-    var currentROI = null;
-    var videoResolution = { width: 1920, height: 1080 };
-
-    (function init() {
-        var params = new URLSearchParams(window.location.search);
-        currentStream = params.get('stream') || 'in-low';
-        hasROI = ROI_STREAMS.indexOf(currentStream) >= 0;
-        var streamName = STREAM_NAMES[currentStream] || '未知';
-        document.title = streamName + ' - 视频监控';
-        document.getElementById('page-title').textContent = streamName;
-        document.getElementById('stream-label').textContent = streamName;
-        var navEl = document.getElementById('nav-' + currentStream);
-        if (navEl) navEl.classList.add('active');
-        if (!hasROI) {
-            document.getElementById('btn-draw-roi').style.display = 'none';
-            document.getElementById('btn-clear-roi').style.display = 'none';
-            document.getElementById('help-text').textContent = '高位摄像头无ROI功能';
-        } else {
-            document.getElementById('help-text').innerHTML =
-                'ROI仅影响推理裁剪区域,<b>上传照片仍为原始全帧</b> | ESC退出绘制模式';
-        }
-        startStream();
-        loadRoiConfig();
-        document.addEventListener('keydown', function(e) {
-            if (e.key === 'Escape' && isDrawingROI) cancelDrawROI();
-        });
-        var img = document.getElementById('video-stream');
-        img.onload = function() {
-            document.getElementById('stream-status').textContent = '🟢 在线';
-            document.getElementById('stream-status').className = 'stream-status';
-        };
-        img.onerror = function() {
-            document.getElementById('stream-status').textContent = '🔴 离线';
-            document.getElementById('stream-status').className = 'stream-status offline';
-            setTimeout(function() {
-                img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-            }, 3000);
-        };
-    })();
-
-    function startStream() {
-        document.getElementById('video-stream').src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-    }
-    function refreshStream() {
-        var img = document.getElementById('video-stream');
-        img.src = '';
-        setTimeout(function() { img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now(); }, 100);
-    }
-    function takeSnapshot() {
-        window.open('/api/stream?stream=' + currentStream + '&t=' + Date.now(), '_blank');
-    }
-
-    function loadRoiConfig() {
-        fetch('/api/roi/config')
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                var roi = null;
-                if (currentStream === 'in-low') roi = data.roi_in;
-                else if (currentStream === 'out-low') roi = data.roi_out;
-                if (data.resolution && data.resolution[currentStream]) {
-                    videoResolution = data.resolution[currentStream];
-                }
-                var infoEl = document.getElementById('roi-info');
-                if (!hasROI) {
-                    infoEl.innerHTML = '<span class="label">高位摄像头 — 无ROI配置</span>';
-                    return;
-                }
-                if (roi && roi.enabled) {
-                    currentROI = { x: roi.x, y: roi.y, w: roi.w, h: roi.h };
-                    infoEl.innerHTML =
-                        '<span class="label">ROI区域:</span> <span class="value">(' +
-                        roi.x + ', ' + roi.y + ') ' + roi.w + '×' + roi.h +
-                        '</span> <span class="label">| 分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height +
-                        '</span><br><span class="label">状态:</span> <span class="value" style="color:#52c41a;">✅ 已启用</span>' +
-                        ' <span class="label">| 推理裁剪,上传全帧</span>';
-                } else {
-                    currentROI = null;
-                    infoEl.innerHTML =
-                        '<span class="label">ROI状态:</span> <span class="value" style="color:#faad14;">⚪ 未启用(全帧推理)</span>' +
-                        '<br><span class="label">分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height + '</span>';
-                }
-            })
-            .catch(function(e) {
-                document.getElementById('roi-info').innerHTML =
-                    '<span class="label" style="color:#f5222d;">⚠️ 配置加载失败</span>';
-            });
-    }
-
-    function startDrawROI() {
-        if (!hasROI) return;
-        isDrawingROI = true;
-        drawStart = null;
-        var img = document.getElementById('video-stream');
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        img.src = '/api/stream?stream=' + currentStream + '&t=' + Date.now();
-        resizeCanvas();
-        canvas.style.display = 'block';
-        btn.classList.add('drawing');
-        btn.innerHTML = '🔲 绘制中...';
-        canvas.onmousedown = onDrawStart;
-        canvas.onmousemove = onDrawMove;
-        canvas.onmouseup = onDrawEnd;
-        canvas.ontouchstart = onTouchStart;
-        canvas.ontouchmove = onTouchMove;
-        canvas.ontouchend = onTouchEnd;
-    }
-
-    function cancelDrawROI() {
-        isDrawingROI = false;
-        drawStart = null;
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        canvas.style.display = 'none';
-        canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
-        btn.classList.remove('drawing');
-        btn.innerHTML = '✏️ 绘制ROI';
-        startStream();
-    }
-
-    function finishDrawROI(rect) {
-        var canvas = document.getElementById('roi-canvas');
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        var roiX = Math.round(rect.x * scaleX);
-        var roiY = Math.round(rect.y * scaleY);
-        var roiW = Math.round(rect.w * scaleX);
-        var roiH = Math.round(rect.h * scaleY);
-        roiX = Math.max(0, Math.min(roiX, videoResolution.width - 1));
-        roiY = Math.max(0, Math.min(roiY, videoResolution.height - 1));
-        roiW = Math.min(roiW, videoResolution.width - roiX);
-        roiH = Math.min(roiH, videoResolution.height - roiY);
-        if (roiW < 50 || roiH < 50) {
-            alert('ROI区域太小(最小50×50像素),请重新绘制');
-            cancelDrawROI();
-            return;
-        }
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (confirm('确认应用' + sideName + 'ROI区域?\n坐标: (' + roiX + ', ' + roiY + ')\n尺寸: ' + roiW + '×' + roiH)) {
-            fetch('/api/roi/update', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ side: side, x: roiX, y: roiY, w: roiW, h: roiH, enabled: true })
-            })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                if (data.success) {
-                    alert('✅ ' + sideName + 'ROI已更新并保存到config.ini');
-                    loadRoiConfig();
-                } else {
-                    alert('❌ 更新失败: ' + (data.message || '未知错误'));
-                }
-            })
-            .catch(function(e) { alert('请求失败: ' + e); });
-        }
-        cancelDrawROI();
-    }
-
-    function clearROI() {
-        if (!hasROI) return;
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (!confirm('确定清除' + sideName + 'ROI区域?\n清除后将使用全帧推理')) return;
-        fetch('/api/roi/update', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ side: side, x: 0, y: 0, w: 0, h: 0, enabled: false })
-        })
-        .then(function(r) { return r.json(); })
-        .then(function(data) {
-            if (data.success) {
-                alert('✅ ROI已清除,恢复全帧推理');
-                loadRoiConfig();
-            } else {
-                alert('❌ 清除失败: ' + (data.message || '未知错误'));
-            }
-        })
-        .catch(function(e) { alert('请求失败: ' + e); });
-    }
-
-    function resizeCanvas() {
-        var wrapper = document.getElementById('video-wrapper');
-        var canvas = document.getElementById('roi-canvas');
-        canvas.width = wrapper.clientWidth;
-        canvas.height = wrapper.clientHeight;
-    }
-
-    function getCanvasPos(e) {
-        var rect = document.getElementById('roi-canvas').getBoundingClientRect();
-        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
-    }
-
-    function onDrawStart(e) { drawStart = getCanvasPos(e); }
-    function onDrawMove(e) {
-        if (!drawStart) return;
-        var canvas = document.getElementById('roi-canvas');
-        var ctx = canvas.getContext('2d');
-        var pos = getCanvasPos(e);
-        ctx.clearRect(0, 0, canvas.width, canvas.height);
-        var rx = Math.min(drawStart.x, pos.x);
-        var ry = Math.min(drawStart.y, pos.y);
-        var rw = Math.abs(pos.x - drawStart.x);
-        var rh = Math.abs(pos.y - drawStart.y);
-        ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
-        ctx.fillRect(rx, ry, rw, rh);
-        ctx.strokeStyle = '#ff0000';
-        ctx.lineWidth = 2;
-        ctx.setLineDash([6, 3]);
-        ctx.strokeRect(rx, ry, rw, rh);
-        ctx.setLineDash([]);
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        ctx.fillStyle = '#ff0000';
-        ctx.font = '14px sans-serif';
-        ctx.fillText(Math.round(rw * scaleX) + '×' + Math.round(rh * scaleY), rx + 4, ry + 18);
-    }
-    function onDrawEnd(e) {
-        if (!drawStart) return;
-        var pos = getCanvasPos(e);
-        var x = Math.min(drawStart.x, pos.x);
-        var y = Math.min(drawStart.y, pos.y);
-        var w = Math.abs(pos.x - drawStart.x);
-        var h = Math.abs(pos.y - drawStart.y);
-        if (w < 5 || h < 5) { cancelDrawROI(); return; }
-        finishDrawROI({ x: x, y: y, w: w, h: h });
-    }
-
-    function onTouchStart(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawStart({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchMove(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawMove({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchEnd(e) {
-        e.preventDefault();
-        var t = e.changedTouches[0];
-        onDrawEnd({ clientX: t.clientX, clientY: t.clientY });
-    }
-    </script>
-</body>
-</html>

+ 0 - 381
assets/web/video.bak4.html

@@ -1,381 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>视频监控</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column;
-        }
-        .top-bar {
-            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-            padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
-            flex-shrink: 0;
-        }
-        .top-bar h1 { font-size: 18px; color: white; }
-        .nav-links { display: flex; gap: 8px; }
-        .nav-link {
-            background: rgba(255,255,255,0.2); color: white; padding: 6px 14px;
-            border-radius: 16px; text-decoration: none; font-size: 13px;
-            transition: background 0.2s; white-space: nowrap;
-        }
-        .nav-link:hover { background: rgba(255,255,255,0.35); }
-        .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
-
-        .video-area {
-            flex: 1; display: flex; flex-direction: column; align-items: center;
-            padding: 16px; overflow: auto;
-        }
-        .video-wrapper {
-            position: relative; background: #000; border-radius: 8px;
-            overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5);
-            max-width: 960px; width: 100%;
-        }
-        .video-wrapper img { width: 100%; display: block; }
-        #roi-canvas {
-            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
-            cursor: crosshair; display: none; z-index: 10;
-        }
-        .stream-label {
-            position: absolute; top: 18px; left: 12px; background: rgba(0,0,0,0.6);
-            color: white; padding: 4px 10px; border-radius: 4px; font-size: 13px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status {
-            position: absolute; top: 18px; right: 12px; background: rgba(0,0,0,0.6);
-            color: #52c41a; padding: 4px 10px; border-radius: 4px; font-size: 12px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status.offline { color: #f5222d; }
-
-        .controls {
-            margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap;
-            justify-content: center; max-width: 960px; width: 100%;
-        }
-        .btn {
-            padding: 10px 20px; border: 1px solid #444; background: #2a2a4a;
-            color: #eee; border-radius: 8px; cursor: pointer; font-size: 14px;
-            transition: all 0.2s; display: flex; align-items: center; gap: 6px;
-        }
-        .btn:hover { background: #3a3a5a; border-color: #667eea; }
-        .btn.primary { background: #667eea; border-color: #667eea; }
-        .btn.primary:hover { background: #5a6ed6; }
-        .btn.danger { background: #ff4d4f; border-color: #ff4d4f; color: white; }
-        .btn.danger:hover { background: #e04445; }
-        .btn.drawing { background: #ff4d4f; color: white; border-color: #ff4d4f; animation: pulse 1.5s infinite; }
-        .btn:disabled { opacity: 0.5; cursor: not-allowed; }
-
-        @keyframes pulse {
-            0%, 100% { opacity: 1; }
-            50% { opacity: 0.7; }
-        }
-
-        .roi-info {
-            margin-top: 12px; padding: 12px 16px; background: #2a2a4a;
-            border-radius: 8px; font-size: 13px; line-height: 1.8;
-            max-width: 960px; width: 100%;
-        }
-        .roi-info .label { color: #999; }
-        .roi-info .value { color: #667eea; font-weight: bold; }
-        .help-text { margin-top: 8px; font-size: 12px; color: #666; text-align: center; }
-
-        @media (max-width: 600px) {
-            .top-bar { flex-direction: column; gap: 8px; }
-            .nav-links { flex-wrap: wrap; justify-content: center; }
-        }
-    </style>
-</head>
-<body>
-    <div class="top-bar">
-        <h1 id="page-title">视频监控</h1>
-        <div class="nav-links">
-            <a href="/" class="nav-link">📊 监控台</a>
-            <a href="/video?stream=in-low" class="nav-link" id="nav-in-low">低位进站</a>
-            <a href="/video?stream=in-high" class="nav-link" id="nav-in-high">高位进站</a>
-            <a href="/video?stream=out-low" class="nav-link" id="nav-out-low">低位出站</a>
-            <a href="/video?stream=out-high" class="nav-link" id="nav-out-high">高位出站</a>
-        </div>
-    </div>
-
-    <div class="video-area">
-        <div class="video-wrapper" id="video-wrapper">
-            <div class="stream-label" id="stream-label">加载中...</div>
-            <div class="stream-status" id="stream-status">⏳ 连接中</div>
-            <img id="video-stream" src="" alt="视频流">
-            <canvas id="roi-canvas"></canvas>
-        </div>
-
-        <div class="controls">
-            <button class="btn primary" onclick="takeSnapshot()">📷 截图</button>
-            <button class="btn" id="btn-draw-roi" onclick="startDrawROI()">✏️ 绘制ROI</button>
-            <button class="btn" id="btn-clear-roi" onclick="clearROI()">🗑️ 清除ROI</button>
-            <button class="btn" onclick="refreshStream()">🔄 刷新视频</button>
-        </div>
-
-        <div class="roi-info" id="roi-info">
-            <span class="label">加载配置中...</span>
-        </div>
-        <div class="help-text" id="help-text"></div>
-    </div>
-
-    <script>
-    var STREAM_NAMES = {
-        'in-low': '低位进站', 'in-high': '高位进站',
-        'out-low': '低位出站', 'out-high': '高位出站'
-    };
-    var ROI_STREAMS = ['in-low', 'out-low'];
-    var currentStream = 'in-low';
-    var hasROI = false;
-    var isDrawingROI = false;
-    var drawStart = null;
-    var currentROI = null;
-    var videoResolution = { width: 1920, height: 1080 };
-
-    (function init() {
-        var params = new URLSearchParams(window.location.search);
-        currentStream = params.get('stream') || 'in-low';
-        hasROI = ROI_STREAMS.indexOf(currentStream) >= 0;
-        var streamName = STREAM_NAMES[currentStream] || '未知';
-        document.title = streamName + ' - 视频监控';
-        document.getElementById('page-title').textContent = streamName;
-        document.getElementById('stream-label').textContent = streamName;
-        var navEl = document.getElementById('nav-' + currentStream);
-        if (navEl) navEl.classList.add('active');
-        if (!hasROI) {
-            document.getElementById('btn-draw-roi').style.display = 'none';
-            document.getElementById('btn-clear-roi').style.display = 'none';
-            document.getElementById('help-text').textContent = '高位摄像头无ROI功能';
-        } else {
-            document.getElementById('help-text').innerHTML =
-                'ROI仅影响推理裁剪区域,<b>上传照片仍为原始全帧</b> | ESC退出绘制模式';
-        }
-        startStream();
-        loadRoiConfig();
-        document.addEventListener('keydown', function(e) {
-            if (e.key === 'Escape' && isDrawingROI) cancelDrawROI();
-        });
-        var img = document.getElementById('video-stream');
-        img.onload = function() {
-            document.getElementById('stream-status').textContent = '🟢 在线';
-            document.getElementById('stream-status').className = 'stream-status';
-        };
-        img.onerror = function() {
-            document.getElementById('stream-status').textContent = '🔴 离线';
-            document.getElementById('stream-status').className = 'stream-status offline';
-            setTimeout(function() {
-                img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-            }, 3000);
-        };
-    })();
-
-    function startStream() {
-        document.getElementById('video-stream').src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-    }
-    function refreshStream() {
-        var img = document.getElementById('video-stream');
-        img.src = '';
-        setTimeout(function() { img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now(); }, 100);
-    }
-    function takeSnapshot() {
-        window.open('/api/stream?stream=' + currentStream + '&t=' + Date.now(), '_blank');
-    }
-
-    function loadRoiConfig() {
-        fetch('/api/roi/config')
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                var roi = null;
-                if (currentStream === 'in-low') roi = data.roi_in;
-                else if (currentStream === 'out-low') roi = data.roi_out;
-                if (data.resolution && data.resolution[currentStream]) {
-                    videoResolution = data.resolution[currentStream];
-                }
-                var infoEl = document.getElementById('roi-info');
-                if (!hasROI) {
-                    infoEl.innerHTML = '<span class="label">高位摄像头 — 无ROI配置</span>';
-                    return;
-                }
-                if (roi && roi.enabled) {
-                    currentROI = { x: roi.x, y: roi.y, w: roi.w, h: roi.h };
-                    infoEl.innerHTML =
-                        '<span class="label">ROI区域:</span> <span class="value">(' +
-                        roi.x + ', ' + roi.y + ') ' + roi.w + '×' + roi.h +
-                        '</span> <span class="label">| 分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height +
-                        '</span><br><span class="label">状态:</span> <span class="value" style="color:#52c41a;">✅ 已启用</span>' +
-                        ' <span class="label">| 推理裁剪,上传全帧</span>';
-                } else {
-                    currentROI = null;
-                    infoEl.innerHTML =
-                        '<span class="label">ROI状态:</span> <span class="value" style="color:#faad14;">⚪ 未启用(全帧推理)</span>' +
-                        '<br><span class="label">分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height + '</span>';
-                }
-            })
-            .catch(function(e) {
-                document.getElementById('roi-info').innerHTML =
-                    '<span class="label" style="color:#f5222d;">⚠️ 配置加载失败</span>';
-            });
-    }
-
-    function startDrawROI() {
-        if (!hasROI) return;
-        isDrawingROI = true;
-        drawStart = null;
-        var img = document.getElementById('video-stream');
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        img.src = '/api/stream?stream=' + currentStream + '&t=' + Date.now();
-        resizeCanvas();
-        canvas.style.display = 'block';
-        btn.classList.add('drawing');
-        btn.innerHTML = '🔲 绘制中...';
-        canvas.onmousedown = onDrawStart;
-        canvas.onmousemove = onDrawMove;
-        canvas.onmouseup = onDrawEnd;
-        canvas.ontouchstart = onTouchStart;
-        canvas.ontouchmove = onTouchMove;
-        canvas.ontouchend = onTouchEnd;
-    }
-
-    function cancelDrawROI() {
-        isDrawingROI = false;
-        drawStart = null;
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        canvas.style.display = 'none';
-        canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
-        btn.classList.remove('drawing');
-        btn.innerHTML = '✏️ 绘制ROI';
-        startStream();
-    }
-
-    function finishDrawROI(rect) {
-        var canvas = document.getElementById('roi-canvas');
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        var roiX = Math.round(rect.x * scaleX);
-        var roiY = Math.round(rect.y * scaleY);
-        var roiW = Math.round(rect.w * scaleX);
-        var roiH = Math.round(rect.h * scaleY);
-        roiX = Math.max(0, Math.min(roiX, videoResolution.width - 1));
-        roiY = Math.max(0, Math.min(roiY, videoResolution.height - 1));
-        roiW = Math.min(roiW, videoResolution.width - roiX);
-        roiH = Math.min(roiH, videoResolution.height - roiY);
-        if (roiW < 50 || roiH < 50) {
-            alert('ROI区域太小(最小50×50像素),请重新绘制');
-            cancelDrawROI();
-            return;
-        }
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (confirm('确认应用' + sideName + 'ROI区域?\n坐标: (' + roiX + ', ' + roiY + ')\n尺寸: ' + roiW + '×' + roiH)) {
-            fetch('/api/roi/update', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ side: side, x: roiX, y: roiY, w: roiW, h: roiH, enabled: true })
-            })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                if (data.success) {
-                    alert('✅ ' + sideName + 'ROI已更新并保存到config.ini');
-                    loadRoiConfig();
-                } else {
-                    alert('❌ 更新失败: ' + (data.message || '未知错误'));
-                }
-            })
-            .catch(function(e) { alert('请求失败: ' + e); });
-        }
-        cancelDrawROI();
-    }
-
-    function clearROI() {
-        if (!hasROI) return;
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (!confirm('确定清除' + sideName + 'ROI区域?\n清除后将使用全帧推理')) return;
-        fetch('/api/roi/update', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ side: side, x: 0, y: 0, w: 0, h: 0, enabled: false })
-        })
-        .then(function(r) { return r.json(); })
-        .then(function(data) {
-            if (data.success) {
-                alert('✅ ROI已清除,恢复全帧推理');
-                loadRoiConfig();
-            } else {
-                alert('❌ 清除失败: ' + (data.message || '未知错误'));
-            }
-        })
-        .catch(function(e) { alert('请求失败: ' + e); });
-    }
-
-    function resizeCanvas() {
-        var wrapper = document.getElementById('video-wrapper');
-        var canvas = document.getElementById('roi-canvas');
-        canvas.width = wrapper.clientWidth;
-        canvas.height = wrapper.clientHeight;
-    }
-
-    function getCanvasPos(e) {
-        var rect = document.getElementById('roi-canvas').getBoundingClientRect();
-        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
-    }
-
-    function onDrawStart(e) { drawStart = getCanvasPos(e); }
-    function onDrawMove(e) {
-        if (!drawStart) return;
-        var canvas = document.getElementById('roi-canvas');
-        var ctx = canvas.getContext('2d');
-        var pos = getCanvasPos(e);
-        ctx.clearRect(0, 0, canvas.width, canvas.height);
-        var rx = Math.min(drawStart.x, pos.x);
-        var ry = Math.min(drawStart.y, pos.y);
-        var rw = Math.abs(pos.x - drawStart.x);
-        var rh = Math.abs(pos.y - drawStart.y);
-        ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
-        ctx.fillRect(rx, ry, rw, rh);
-        ctx.strokeStyle = '#ff0000';
-        ctx.lineWidth = 2;
-        ctx.setLineDash([6, 3]);
-        ctx.strokeRect(rx, ry, rw, rh);
-        ctx.setLineDash([]);
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        ctx.fillStyle = '#ff0000';
-        ctx.font = '14px sans-serif';
-        ctx.fillText(Math.round(rw * scaleX) + '×' + Math.round(rh * scaleY), rx + 4, ry + 18);
-    }
-    function onDrawEnd(e) {
-        if (!drawStart) return;
-        var pos = getCanvasPos(e);
-        var x = Math.min(drawStart.x, pos.x);
-        var y = Math.min(drawStart.y, pos.y);
-        var w = Math.abs(pos.x - drawStart.x);
-        var h = Math.abs(pos.y - drawStart.y);
-        if (w < 5 || h < 5) { cancelDrawROI(); return; }
-        finishDrawROI({ x: x, y: y, w: w, h: h });
-    }
-
-    function onTouchStart(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawStart({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchMove(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawMove({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchEnd(e) {
-        e.preventDefault();
-        var t = e.changedTouches[0];
-        onDrawEnd({ clientX: t.clientX, clientY: t.clientY });
-    }
-    </script>
-</body>
-</html>

+ 0 - 407
assets/web/video.html

@@ -1,407 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>视频监控</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column;
-        }
-        .top-bar {
-            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-            padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
-            flex-shrink: 0;
-        }
-        .top-bar h1 { font-size: 18px; color: white; }
-        .nav-links { display: flex; gap: 8px; }
-        .nav-link {
-            background: rgba(255,255,255,0.2); color: white; padding: 6px 14px;
-            border-radius: 16px; text-decoration: none; font-size: 13px;
-            transition: background 0.2s; white-space: nowrap;
-        }
-        .nav-link:hover { background: rgba(255,255,255,0.35); }
-        .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
-
-        .video-area {
-            flex: 1; display: flex; flex-direction: column; align-items: center;
-            padding: 16px; overflow: auto;
-        }
-        .video-wrapper {
-            position: relative; background: #000; border-radius: 8px;
-            overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5);
-            max-width: 960px; width: 100%;
-        }
-        .video-wrapper img { width: 100%; display: block; }
-        #roi-canvas {
-            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
-            cursor: crosshair; display: none; z-index: 10;
-        }
-        .stream-label {
-            position: absolute; top: 18px; left: 12px; background: rgba(0,0,0,0.6);
-            color: white; padding: 4px 10px; border-radius: 4px; font-size: 13px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status {
-            position: absolute; top: 18px; right: 12px; background: rgba(0,0,0,0.6);
-            color: #52c41a; padding: 4px 10px; border-radius: 4px; font-size: 12px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status.offline { color: #f5222d; }
-
-        .controls {
-            margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap;
-            justify-content: center; max-width: 960px; width: 100%;
-        }
-        .btn {
-            padding: 10px 20px; border: 1px solid #444; background: #2a2a4a;
-            color: #eee; border-radius: 8px; cursor: pointer; font-size: 14px;
-            transition: all 0.2s; display: flex; align-items: center; gap: 6px;
-        }
-        .btn:hover { background: #3a3a5a; border-color: #667eea; }
-        .btn.primary { background: #667eea; border-color: #667eea; }
-        .btn.primary:hover { background: #5a6ed6; }
-        .btn.danger { background: #ff4d4f; border-color: #ff4d4f; color: white; }
-        .btn.danger:hover { background: #e04445; }
-        .btn.drawing { background: #ff4d4f; color: white; border-color: #ff4d4f; animation: pulse 1.5s infinite; }
-        .btn:disabled { opacity: 0.5; cursor: not-allowed; }
-
-        @keyframes pulse {
-            0%, 100% { opacity: 1; }
-            50% { opacity: 0.7; }
-        }
-
-        .roi-info {
-            margin-top: 12px; padding: 12px 16px; background: #2a2a4a;
-            border-radius: 8px; font-size: 13px; line-height: 1.8;
-            max-width: 960px; width: 100%;
-        }
-        .roi-info .label { color: #999; }
-        .roi-info .value { color: #667eea; font-weight: bold; }
-        .help-text { margin-top: 8px; font-size: 12px; color: #666; text-align: center; }
-
-        @media (max-width: 600px) {
-            .top-bar { flex-direction: column; gap: 8px; }
-            .nav-links { flex-wrap: wrap; justify-content: center; }
-        }
-    
-        @media (max-width: 600px) {
-            .top-bar { padding: 10px 12px; }
-            .top-bar h1 { font-size: 15px; }
-            .nav-link { padding: 4px 10px; font-size: 12px; }
-            .video-area { padding: 8px; }
-            .controls { gap: 6px; }
-            .btn { padding: 8px 14px; font-size: 13px; }
-            .roi-info { padding: 10px 12px; font-size: 12px; }
-        }
-    </style>
-</head>
-<body>
-    <div class="top-bar">
-        <h1 id="page-title">视频监控</h1>
-        <div class="nav-links">
-            <a href="/" class="nav-link">📊 监控台</a>
-            <a href="/video?stream=in-low" class="nav-link" id="nav-in-low">低位进站</a>
-            <a href="/video?stream=in-high" class="nav-link" id="nav-in-high">高位进站</a>
-            <a href="/video?stream=out-low" class="nav-link" id="nav-out-low">低位出站</a>
-            <a href="/video?stream=out-high" class="nav-link" id="nav-out-high">高位出站</a>
-            <a href="/help" class="nav-link" id="nav-help" style="display:none;">📖 帮助</a>
-        </div>
-    </div>
-
-    <div class="video-area">
-        <div class="video-wrapper" id="video-wrapper">
-            <div class="stream-label" id="stream-label">加载中...</div>
-            <div class="stream-status" id="stream-status">⏳ 连接中</div>
-            <div class="stream-status" id="shared-badge" style="display:none; top:18px; right:100px; background:rgba(102,126,234,0.7); color:white;">🔗 共享模式</div>
-            <img id="video-stream" src="" alt="视频流">
-            <canvas id="roi-canvas"></canvas>
-        </div>
-
-        <div class="controls">
-            <button class="btn primary" onclick="takeSnapshot()">📷 截图</button>
-            <button class="btn" id="btn-draw-roi" onclick="startDrawROI()">✏️ 绘制ROI</button>
-            <button class="btn" id="btn-clear-roi" onclick="clearROI()">🗑️ 清除ROI</button>
-            <button class="btn" onclick="refreshStream()">🔄 刷新视频</button>
-        </div>
-
-        <div class="roi-info" id="roi-info">
-            <span class="label">加载配置中...</span>
-        </div>
-        <div class="help-text" id="help-text"></div>
-    </div>
-
-    <script>
-    function getCookie(n){let m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}
-    var STREAM_NAMES = {
-        'in-low': '低位进站', 'in-high': '高位进站',
-        'out-low': '低位出站', 'out-high': '高位出站'
-    };
-    var ROI_STREAMS = ['in-low', 'out-low'];
-    var currentStream = 'in-low';
-    var hasROI = false;
-    var isDrawingROI = false;
-    var drawStart = null;
-    var currentROI = null;
-    var videoResolution = { width: 1920, height: 1080 };
-
-    (function init() {
-        var params = new URLSearchParams(window.location.search);
-        currentStream = params.get('stream') || 'in-low';
-        hasROI = ROI_STREAMS.indexOf(currentStream) >= 0;
-        var streamName = STREAM_NAMES[currentStream] || '未知';
-        document.title = streamName + ' - 视频监控';
-        document.getElementById('page-title').textContent = streamName;
-        document.getElementById('stream-label').textContent = streamName;
-        var navEl = document.getElementById('nav-' + currentStream);
-        if (navEl) navEl.classList.add('active');
-        if (!hasROI) {
-            document.getElementById('btn-draw-roi').style.display = 'none';
-            document.getElementById('btn-clear-roi').style.display = 'none';
-            document.getElementById('help-text').textContent = '高位摄像头无ROI功能';
-        } else {
-            document.getElementById('help-text').innerHTML =
-                'ROI仅影响推理裁剪区域,<b>上传照片仍为原始全帧</b> | ESC退出绘制模式';
-        }
-        startStream();
-        loadRoiConfig();
-        document.addEventListener('keydown', function(e) {
-            if (e.key === 'Escape' && isDrawingROI) cancelDrawROI();
-        });
-        var img = document.getElementById('video-stream');
-        img.onload = function() {
-            document.getElementById('stream-status').textContent = '🟢 在线';
-            document.getElementById('stream-status').className = 'stream-status';
-        };
-        img.onerror = function() {
-            document.getElementById('stream-status').textContent = '🔴 离线';
-            document.getElementById('stream-status').className = 'stream-status offline';
-            setTimeout(function() {
-                img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-            }, 3000);
-        };
-    })();
-
-    function startStream() {
-        document.getElementById('video-stream').src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-    }
-    function refreshStream() {
-        var img = document.getElementById('video-stream');
-        img.src = '';
-        setTimeout(function() { img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now(); }, 100);
-    }
-    function takeSnapshot() {
-        window.open('/api/stream?stream=' + currentStream + '&t=' + Date.now(), '_blank');
-    }
-
-    function loadRoiConfig() {
-        fetch('/api/roi/config')
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                var roi = null;
-                if (currentStream === 'in-low') roi = data.roi_in;
-                else if (currentStream === 'out-low') roi = data.roi_out;
-                if (data.resolution && data.resolution[currentStream]) {
-                    videoResolution = data.resolution[currentStream];
-                }
-                // ✅ fix24-v9: 共享模式下出站页面显示提示
-                if (data.shared_mode && currentStream === 'out-low') {
-                    var badge = document.getElementById('shared-badge');
-                    if (badge) badge.style.display = 'block';
-                }
-                var infoEl = document.getElementById('roi-info');
-                if (!hasROI) {
-                    infoEl.innerHTML = '<span class="label">高位摄像头 — 无ROI配置</span>';
-                    return;
-                }
-                if (roi && roi.enabled) {
-                    currentROI = { x: roi.x, y: roi.y, w: roi.w, h: roi.h };
-                    infoEl.innerHTML =
-                        '<span class="label">ROI区域:</span> <span class="value">(' +
-                        roi.x + ', ' + roi.y + ') ' + roi.w + '×' + roi.h +
-                        '</span> <span class="label">| 分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height +
-                        '</span><br><span class="label">状态:</span> <span class="value" style="color:#52c41a;">✅ 已启用</span>' +
-                        ' <span class="label">| 推理裁剪,上传全帧</span>';
-                } else {
-                    currentROI = null;
-                    infoEl.innerHTML =
-                        '<span class="label">ROI状态:</span> <span class="value" style="color:#faad14;">⚪ 未启用(全帧推理)</span>' +
-                        '<br><span class="label">分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height + '</span>';
-                }
-            })
-            .catch(function(e) {
-                document.getElementById('roi-info').innerHTML =
-                    '<span class="label" style="color:#f5222d;">⚠️ 配置加载失败</span>';
-            });
-    }
-
-    function startDrawROI() {
-        if (!hasROI) return;
-        isDrawingROI = true;
-        drawStart = null;
-        var img = document.getElementById('video-stream');
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        img.src = '/api/stream?stream=' + currentStream + '&t=' + Date.now();
-        resizeCanvas();
-        canvas.style.display = 'block';
-        btn.classList.add('drawing');
-        btn.innerHTML = '🔲 绘制中...';
-        canvas.onmousedown = onDrawStart;
-        canvas.onmousemove = onDrawMove;
-        canvas.onmouseup = onDrawEnd;
-        canvas.ontouchstart = onTouchStart;
-        canvas.ontouchmove = onTouchMove;
-        canvas.ontouchend = onTouchEnd;
-    }
-
-    function cancelDrawROI() {
-        isDrawingROI = false;
-        drawStart = null;
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        canvas.style.display = 'none';
-        canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
-        btn.classList.remove('drawing');
-        btn.innerHTML = '✏️ 绘制ROI';
-        startStream();
-    }
-
-    function finishDrawROI(rect) {
-        var canvas = document.getElementById('roi-canvas');
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        var roiX = Math.round(rect.x * scaleX);
-        var roiY = Math.round(rect.y * scaleY);
-        var roiW = Math.round(rect.w * scaleX);
-        var roiH = Math.round(rect.h * scaleY);
-        roiX = Math.max(0, Math.min(roiX, videoResolution.width - 1));
-        roiY = Math.max(0, Math.min(roiY, videoResolution.height - 1));
-        roiW = Math.min(roiW, videoResolution.width - roiX);
-        roiH = Math.min(roiH, videoResolution.height - roiY);
-        if (roiW < 50 || roiH < 50) {
-            alert('ROI区域太小(最小50×50像素),请重新绘制');
-            cancelDrawROI();
-            return;
-        }
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (confirm('确认应用' + sideName + 'ROI区域?\n坐标: (' + roiX + ', ' + roiY + ')\n尺寸: ' + roiW + '×' + roiH)) {
-            fetch('/api/roi/update', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCookie('csrf_token') },
-                body: JSON.stringify({ side: side, x: roiX, y: roiY, w: roiW, h: roiH, enabled: true })
-            })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                if (data.success) {
-                    alert('✅ ' + sideName + 'ROI已更新并保存到config.ini');
-                    loadRoiConfig();
-                } else {
-                    alert('❌ 更新失败: ' + (data.message || '未知错误'));
-                }
-            })
-            .catch(function(e) { alert('请求失败: ' + e); });
-        }
-        cancelDrawROI();
-    }
-
-    function clearROI() {
-        if (!hasROI) return;
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (!confirm('确定清除' + sideName + 'ROI区域?\n清除后将使用全帧推理')) return;
-        fetch('/api/roi/update', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCookie('csrf_token') },
-            body: JSON.stringify({ side: side, x: 0, y: 0, w: 0, h: 0, enabled: false })
-        })
-        .then(function(r) { return r.json(); })
-        .then(function(data) {
-            if (data.success) {
-                alert('✅ ROI已清除,恢复全帧推理');
-                loadRoiConfig();
-            } else {
-                alert('❌ 清除失败: ' + (data.message || '未知错误'));
-            }
-        })
-        .catch(function(e) { alert('请求失败: ' + e); });
-    }
-
-    function resizeCanvas() {
-        var wrapper = document.getElementById('video-wrapper');
-        var canvas = document.getElementById('roi-canvas');
-        canvas.width = wrapper.clientWidth;
-        canvas.height = wrapper.clientHeight;
-    }
-
-    function getCanvasPos(e) {
-        var rect = document.getElementById('roi-canvas').getBoundingClientRect();
-        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
-    }
-
-    function onDrawStart(e) { drawStart = getCanvasPos(e); }
-    function onDrawMove(e) {
-        if (!drawStart) return;
-        var canvas = document.getElementById('roi-canvas');
-        var ctx = canvas.getContext('2d');
-        var pos = getCanvasPos(e);
-        ctx.clearRect(0, 0, canvas.width, canvas.height);
-        var rx = Math.min(drawStart.x, pos.x);
-        var ry = Math.min(drawStart.y, pos.y);
-        var rw = Math.abs(pos.x - drawStart.x);
-        var rh = Math.abs(pos.y - drawStart.y);
-        ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
-        ctx.fillRect(rx, ry, rw, rh);
-        ctx.strokeStyle = '#ff0000';
-        ctx.lineWidth = 2;
-        ctx.setLineDash([6, 3]);
-        ctx.strokeRect(rx, ry, rw, rh);
-        ctx.setLineDash([]);
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        ctx.fillStyle = '#ff0000';
-        ctx.font = '14px sans-serif';
-        ctx.fillText(Math.round(rw * scaleX) + '×' + Math.round(rh * scaleY), rx + 4, ry + 18);
-    }
-    function onDrawEnd(e) {
-        if (!drawStart) return;
-        var pos = getCanvasPos(e);
-        var x = Math.min(drawStart.x, pos.x);
-        var y = Math.min(drawStart.y, pos.y);
-        var w = Math.abs(pos.x - drawStart.x);
-        var h = Math.abs(pos.y - drawStart.y);
-        if (w < 5 || h < 5) { cancelDrawROI(); return; }
-        finishDrawROI({ x: x, y: y, w: w, h: h });
-    }
-
-    function onTouchStart(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawStart({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchMove(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawMove({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchEnd(e) {
-        e.preventDefault();
-        var t = e.changedTouches[0];
-        onDrawEnd({ clientX: t.clientX, clientY: t.clientY });
-    }
-
-    // 角色检查:仅超级管理员可看到帮助链接
-    fetch('/api/auth/status', {credentials:'same-origin'}).then(r=>r.json()).then(data=>{
-        if(data.authenticated && data.role >= 2){
-            var el = document.getElementById('nav-help');
-            if(el) el.style.display = '';
-        }
-    }).catch(()=>{});
-    </script>
-</body>
-</html>

+ 0 - 381
assets/web/video3del.html

@@ -1,381 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>视频监控</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column;
-        }
-        .top-bar {
-            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-            padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
-            flex-shrink: 0;
-        }
-        .top-bar h1 { font-size: 18px; color: white; }
-        .nav-links { display: flex; gap: 8px; }
-        .nav-link {
-            background: rgba(255,255,255,0.2); color: white; padding: 6px 14px;
-            border-radius: 16px; text-decoration: none; font-size: 13px;
-            transition: background 0.2s; white-space: nowrap;
-        }
-        .nav-link:hover { background: rgba(255,255,255,0.35); }
-        .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
-
-        .video-area {
-            flex: 1; display: flex; flex-direction: column; align-items: center;
-            padding: 16px; overflow: auto;
-        }
-        .video-wrapper {
-            position: relative; background: #000; border-radius: 8px;
-            overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5);
-            max-width: 960px; width: 100%;
-        }
-        .video-wrapper img { width: 100%; display: block; }
-        #roi-canvas {
-            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
-            cursor: crosshair; display: none; z-index: 10;
-        }
-        .stream-label {
-            position: absolute; top: 18px; left: 12px; background: rgba(0,0,0,0.6);
-            color: white; padding: 4px 10px; border-radius: 4px; font-size: 13px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status {
-            position: absolute; top: 18px; right: 12px; background: rgba(0,0,0,0.6);
-            color: #52c41a; padding: 4px 10px; border-radius: 4px; font-size: 12px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status.offline { color: #f5222d; }
-
-        .controls {
-            margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap;
-            justify-content: center; max-width: 960px; width: 100%;
-        }
-        .btn {
-            padding: 10px 20px; border: 1px solid #444; background: #2a2a4a;
-            color: #eee; border-radius: 8px; cursor: pointer; font-size: 14px;
-            transition: all 0.2s; display: flex; align-items: center; gap: 6px;
-        }
-        .btn:hover { background: #3a3a5a; border-color: #667eea; }
-        .btn.primary { background: #667eea; border-color: #667eea; }
-        .btn.primary:hover { background: #5a6ed6; }
-        .btn.danger { background: #ff4d4f; border-color: #ff4d4f; color: white; }
-        .btn.danger:hover { background: #e04445; }
-        .btn.drawing { background: #ff4d4f; color: white; border-color: #ff4d4f; animation: pulse 1.5s infinite; }
-        .btn:disabled { opacity: 0.5; cursor: not-allowed; }
-
-        @keyframes pulse {
-            0%, 100% { opacity: 1; }
-            50% { opacity: 0.7; }
-        }
-
-        .roi-info {
-            margin-top: 12px; padding: 12px 16px; background: #2a2a4a;
-            border-radius: 8px; font-size: 13px; line-height: 1.8;
-            max-width: 960px; width: 100%;
-        }
-        .roi-info .label { color: #999; }
-        .roi-info .value { color: #667eea; font-weight: bold; }
-        .help-text { margin-top: 8px; font-size: 12px; color: #666; text-align: center; }
-
-        @media (max-width: 600px) {
-            .top-bar { flex-direction: column; gap: 8px; }
-            .nav-links { flex-wrap: wrap; justify-content: center; }
-        }
-    </style>
-</head>
-<body>
-    <div class="top-bar">
-        <h1 id="page-title">视频监控</h1>
-        <div class="nav-links">
-            <a href="/" class="nav-link">📊 监控台</a>
-            <a href="/video?stream=in-low" class="nav-link" id="nav-in-low">低位进站</a>
-            <a href="/video?stream=in-high" class="nav-link" id="nav-in-high">高位进站</a>
-            <a href="/video?stream=out-low" class="nav-link" id="nav-out-low">低位出站</a>
-            <a href="/video?stream=out-high" class="nav-link" id="nav-out-high">高位出站</a>
-        </div>
-    </div>
-
-    <div class="video-area">
-        <div class="video-wrapper" id="video-wrapper">
-            <div class="stream-label" id="stream-label">加载中...</div>
-            <div class="stream-status" id="stream-status">⏳ 连接中</div>
-            <img id="video-stream" src="" alt="视频流">
-            <canvas id="roi-canvas"></canvas>
-        </div>
-
-        <div class="controls">
-            <button class="btn primary" onclick="takeSnapshot()">📷 截图</button>
-            <button class="btn" id="btn-draw-roi" onclick="startDrawROI()">✏️ 绘制ROI</button>
-            <button class="btn" id="btn-clear-roi" onclick="clearROI()">🗑️ 清除ROI</button>
-            <button class="btn" onclick="refreshStream()">🔄 刷新视频</button>
-        </div>
-
-        <div class="roi-info" id="roi-info">
-            <span class="label">加载配置中...</span>
-        </div>
-        <div class="help-text" id="help-text"></div>
-    </div>
-
-    <script>
-    var STREAM_NAMES = {
-        'in-low': '低位进站', 'in-high': '高位进站',
-        'out-low': '低位出站', 'out-high': '高位出站'
-    };
-    var ROI_STREAMS = ['in-low', 'out-low'];
-    var currentStream = 'in-low';
-    var hasROI = false;
-    var isDrawingROI = false;
-    var drawStart = null;
-    var currentROI = null;
-    var videoResolution = { width: 1920, height: 1080 };
-
-    (function init() {
-        var params = new URLSearchParams(window.location.search);
-        currentStream = params.get('stream') || 'in-low';
-        hasROI = ROI_STREAMS.indexOf(currentStream) >= 0;
-        var streamName = STREAM_NAMES[currentStream] || '未知';
-        document.title = streamName + ' - 视频监控';
-        document.getElementById('page-title').textContent = streamName;
-        document.getElementById('stream-label').textContent = streamName;
-        var navEl = document.getElementById('nav-' + currentStream);
-        if (navEl) navEl.classList.add('active');
-        if (!hasROI) {
-            document.getElementById('btn-draw-roi').style.display = 'none';
-            document.getElementById('btn-clear-roi').style.display = 'none';
-            document.getElementById('help-text').textContent = '高位摄像头无ROI功能';
-        } else {
-            document.getElementById('help-text').innerHTML =
-                'ROI仅影响推理裁剪区域,<b>上传照片仍为原始全帧</b> | ESC退出绘制模式';
-        }
-        startStream();
-        loadRoiConfig();
-        document.addEventListener('keydown', function(e) {
-            if (e.key === 'Escape' && isDrawingROI) cancelDrawROI();
-        });
-        var img = document.getElementById('video-stream');
-        img.onload = function() {
-            document.getElementById('stream-status').textContent = '🟢 在线';
-            document.getElementById('stream-status').className = 'stream-status';
-        };
-        img.onerror = function() {
-            document.getElementById('stream-status').textContent = '🔴 离线';
-            document.getElementById('stream-status').className = 'stream-status offline';
-            setTimeout(function() {
-                img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-            }, 3000);
-        };
-    })();
-
-    function startStream() {
-        document.getElementById('video-stream').src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-    }
-    function refreshStream() {
-        var img = document.getElementById('video-stream');
-        img.src = '';
-        setTimeout(function() { img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now(); }, 100);
-    }
-    function takeSnapshot() {
-        window.open('/api/stream?stream=' + currentStream + '&t=' + Date.now(), '_blank');
-    }
-
-    function loadRoiConfig() {
-        fetch('/api/roi/config')
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                var roi = null;
-                if (currentStream === 'in-low') roi = data.roi_in;
-                else if (currentStream === 'out-low') roi = data.roi_out;
-                if (data.resolution && data.resolution[currentStream]) {
-                    videoResolution = data.resolution[currentStream];
-                }
-                var infoEl = document.getElementById('roi-info');
-                if (!hasROI) {
-                    infoEl.innerHTML = '<span class="label">高位摄像头 — 无ROI配置</span>';
-                    return;
-                }
-                if (roi && roi.enabled) {
-                    currentROI = { x: roi.x, y: roi.y, w: roi.w, h: roi.h };
-                    infoEl.innerHTML =
-                        '<span class="label">ROI区域:</span> <span class="value">(' +
-                        roi.x + ', ' + roi.y + ') ' + roi.w + '×' + roi.h +
-                        '</span> <span class="label">| 分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height +
-                        '</span><br><span class="label">状态:</span> <span class="value" style="color:#52c41a;">✅ 已启用</span>' +
-                        ' <span class="label">| 推理裁剪,上传全帧</span>';
-                } else {
-                    currentROI = null;
-                    infoEl.innerHTML =
-                        '<span class="label">ROI状态:</span> <span class="value" style="color:#faad14;">⚪ 未启用(全帧推理)</span>' +
-                        '<br><span class="label">分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height + '</span>';
-                }
-            })
-            .catch(function(e) {
-                document.getElementById('roi-info').innerHTML =
-                    '<span class="label" style="color:#f5222d;">⚠️ 配置加载失败</span>';
-            });
-    }
-
-    function startDrawROI() {
-        if (!hasROI) return;
-        isDrawingROI = true;
-        drawStart = null;
-        var img = document.getElementById('video-stream');
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        img.src = '/api/stream?stream=' + currentStream + '&t=' + Date.now();
-        resizeCanvas();
-        canvas.style.display = 'block';
-        btn.classList.add('drawing');
-        btn.innerHTML = '🔲 绘制中...';
-        canvas.onmousedown = onDrawStart;
-        canvas.onmousemove = onDrawMove;
-        canvas.onmouseup = onDrawEnd;
-        canvas.ontouchstart = onTouchStart;
-        canvas.ontouchmove = onTouchMove;
-        canvas.ontouchend = onTouchEnd;
-    }
-
-    function cancelDrawROI() {
-        isDrawingROI = false;
-        drawStart = null;
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        canvas.style.display = 'none';
-        canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
-        btn.classList.remove('drawing');
-        btn.innerHTML = '✏️ 绘制ROI';
-        startStream();
-    }
-
-    function finishDrawROI(rect) {
-        var canvas = document.getElementById('roi-canvas');
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        var roiX = Math.round(rect.x * scaleX);
-        var roiY = Math.round(rect.y * scaleY);
-        var roiW = Math.round(rect.w * scaleX);
-        var roiH = Math.round(rect.h * scaleY);
-        roiX = Math.max(0, Math.min(roiX, videoResolution.width - 1));
-        roiY = Math.max(0, Math.min(roiY, videoResolution.height - 1));
-        roiW = Math.min(roiW, videoResolution.width - roiX);
-        roiH = Math.min(roiH, videoResolution.height - roiY);
-        if (roiW < 50 || roiH < 50) {
-            alert('ROI区域太小(最小50×50像素),请重新绘制');
-            cancelDrawROI();
-            return;
-        }
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (confirm('确认应用' + sideName + 'ROI区域?\n坐标: (' + roiX + ', ' + roiY + ')\n尺寸: ' + roiW + '×' + roiH)) {
-            fetch('/api/roi/update', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ side: side, x: roiX, y: roiY, w: roiW, h: roiH, enabled: true })
-            })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                if (data.success) {
-                    alert('✅ ' + sideName + 'ROI已更新并保存到config.ini');
-                    loadRoiConfig();
-                } else {
-                    alert('❌ 更新失败: ' + (data.message || '未知错误'));
-                }
-            })
-            .catch(function(e) { alert('请求失败: ' + e); });
-        }
-        cancelDrawROI();
-    }
-
-    function clearROI() {
-        if (!hasROI) return;
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (!confirm('确定清除' + sideName + 'ROI区域?\n清除后将使用全帧推理')) return;
-        fetch('/api/roi/update', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ side: side, x: 0, y: 0, w: 0, h: 0, enabled: false })
-        })
-        .then(function(r) { return r.json(); })
-        .then(function(data) {
-            if (data.success) {
-                alert('✅ ROI已清除,恢复全帧推理');
-                loadRoiConfig();
-            } else {
-                alert('❌ 清除失败: ' + (data.message || '未知错误'));
-            }
-        })
-        .catch(function(e) { alert('请求失败: ' + e); });
-    }
-
-    function resizeCanvas() {
-        var wrapper = document.getElementById('video-wrapper');
-        var canvas = document.getElementById('roi-canvas');
-        canvas.width = wrapper.clientWidth;
-        canvas.height = wrapper.clientHeight;
-    }
-
-    function getCanvasPos(e) {
-        var rect = document.getElementById('roi-canvas').getBoundingClientRect();
-        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
-    }
-
-    function onDrawStart(e) { drawStart = getCanvasPos(e); }
-    function onDrawMove(e) {
-        if (!drawStart) return;
-        var canvas = document.getElementById('roi-canvas');
-        var ctx = canvas.getContext('2d');
-        var pos = getCanvasPos(e);
-        ctx.clearRect(0, 0, canvas.width, canvas.height);
-        var rx = Math.min(drawStart.x, pos.x);
-        var ry = Math.min(drawStart.y, pos.y);
-        var rw = Math.abs(pos.x - drawStart.x);
-        var rh = Math.abs(pos.y - drawStart.y);
-        ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
-        ctx.fillRect(rx, ry, rw, rh);
-        ctx.strokeStyle = '#ff0000';
-        ctx.lineWidth = 2;
-        ctx.setLineDash([6, 3]);
-        ctx.strokeRect(rx, ry, rw, rh);
-        ctx.setLineDash([]);
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        ctx.fillStyle = '#ff0000';
-        ctx.font = '14px sans-serif';
-        ctx.fillText(Math.round(rw * scaleX) + '×' + Math.round(rh * scaleY), rx + 4, ry + 18);
-    }
-    function onDrawEnd(e) {
-        if (!drawStart) return;
-        var pos = getCanvasPos(e);
-        var x = Math.min(drawStart.x, pos.x);
-        var y = Math.min(drawStart.y, pos.y);
-        var w = Math.abs(pos.x - drawStart.x);
-        var h = Math.abs(pos.y - drawStart.y);
-        if (w < 5 || h < 5) { cancelDrawROI(); return; }
-        finishDrawROI({ x: x, y: y, w: w, h: h });
-    }
-
-    function onTouchStart(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawStart({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchMove(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawMove({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchEnd(e) {
-        e.preventDefault();
-        var t = e.changedTouches[0];
-        onDrawEnd({ clientX: t.clientX, clientY: t.clientY });
-    }
-    </script>
-</body>
-</html>

+ 0 - 381
assets/web/videobak3.html

@@ -1,381 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>视频监控</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body {
-            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-            background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column;
-        }
-        .top-bar {
-            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-            padding: 12px 20px; display: flex; align-items: center; justify-content: space-between;
-            flex-shrink: 0;
-        }
-        .top-bar h1 { font-size: 18px; color: white; }
-        .nav-links { display: flex; gap: 8px; }
-        .nav-link {
-            background: rgba(255,255,255,0.2); color: white; padding: 6px 14px;
-            border-radius: 16px; text-decoration: none; font-size: 13px;
-            transition: background 0.2s; white-space: nowrap;
-        }
-        .nav-link:hover { background: rgba(255,255,255,0.35); }
-        .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
-
-        .video-area {
-            flex: 1; display: flex; flex-direction: column; align-items: center;
-            padding: 16px; overflow: auto;
-        }
-        .video-wrapper {
-            position: relative; background: #000; border-radius: 8px;
-            overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.5);
-            max-width: 960px; width: 100%;
-        }
-        .video-wrapper img { width: 100%; display: block; }
-        #roi-canvas {
-            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
-            cursor: crosshair; display: none; z-index: 10;
-        }
-        .stream-label {
-            position: absolute; top: 8px; left: 12px; background: rgba(0,0,0,0.6);
-            color: white; padding: 4px 10px; border-radius: 4px; font-size: 13px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status {
-            position: absolute; top: 8px; right: 12px; background: rgba(0,0,0,0.6);
-            color: #52c41a; padding: 4px 10px; border-radius: 4px; font-size: 12px;
-            pointer-events: none; z-index: 5;
-        }
-        .stream-status.offline { color: #f5222d; }
-
-        .controls {
-            margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap;
-            justify-content: center; max-width: 960px; width: 100%;
-        }
-        .btn {
-            padding: 10px 20px; border: 1px solid #444; background: #2a2a4a;
-            color: #eee; border-radius: 8px; cursor: pointer; font-size: 14px;
-            transition: all 0.2s; display: flex; align-items: center; gap: 6px;
-        }
-        .btn:hover { background: #3a3a5a; border-color: #667eea; }
-        .btn.primary { background: #667eea; border-color: #667eea; }
-        .btn.primary:hover { background: #5a6ed6; }
-        .btn.danger { background: #ff4d4f; border-color: #ff4d4f; color: white; }
-        .btn.danger:hover { background: #e04445; }
-        .btn.drawing { background: #ff4d4f; color: white; border-color: #ff4d4f; animation: pulse 1.5s infinite; }
-        .btn:disabled { opacity: 0.5; cursor: not-allowed; }
-
-        @keyframes pulse {
-            0%, 100% { opacity: 1; }
-            50% { opacity: 0.7; }
-        }
-
-        .roi-info {
-            margin-top: 12px; padding: 12px 16px; background: #2a2a4a;
-            border-radius: 8px; font-size: 13px; line-height: 1.8;
-            max-width: 960px; width: 100%;
-        }
-        .roi-info .label { color: #999; }
-        .roi-info .value { color: #667eea; font-weight: bold; }
-        .help-text { margin-top: 8px; font-size: 12px; color: #666; text-align: center; }
-
-        @media (max-width: 600px) {
-            .top-bar { flex-direction: column; gap: 8px; }
-            .nav-links { flex-wrap: wrap; justify-content: center; }
-        }
-    </style>
-</head>
-<body>
-    <div class="top-bar">
-        <h1 id="page-title">视频监控</h1>
-        <div class="nav-links">
-            <a href="/" class="nav-link">📊 监控台</a>
-            <a href="/video?stream=in-low" class="nav-link" id="nav-in-low">低位进站</a>
-            <a href="/video?stream=in-high" class="nav-link" id="nav-in-high">高位进站</a>
-            <a href="/video?stream=out-low" class="nav-link" id="nav-out-low">低位出站</a>
-            <a href="/video?stream=out-high" class="nav-link" id="nav-out-high">高位出站</a>
-        </div>
-    </div>
-
-    <div class="video-area">
-        <div class="video-wrapper" id="video-wrapper">
-            <div class="stream-label" id="stream-label">加载中...</div>
-            <div class="stream-status" id="stream-status">⏳ 连接中</div>
-            <img id="video-stream" src="" alt="视频流">
-            <canvas id="roi-canvas"></canvas>
-        </div>
-
-        <div class="controls">
-            <button class="btn primary" onclick="takeSnapshot()">📷 截图</button>
-            <button class="btn" id="btn-draw-roi" onclick="startDrawROI()">✏️ 绘制ROI</button>
-            <button class="btn" id="btn-clear-roi" onclick="clearROI()">🗑️ 清除ROI</button>
-            <button class="btn" onclick="refreshStream()">🔄 刷新视频</button>
-        </div>
-
-        <div class="roi-info" id="roi-info">
-            <span class="label">加载配置中...</span>
-        </div>
-        <div class="help-text" id="help-text"></div>
-    </div>
-
-    <script>
-    var STREAM_NAMES = {
-        'in-low': '低位进站', 'in-high': '高位进站',
-        'out-low': '低位出站', 'out-high': '高位出站'
-    };
-    var ROI_STREAMS = ['in-low', 'out-low'];
-    var currentStream = 'in-low';
-    var hasROI = false;
-    var isDrawingROI = false;
-    var drawStart = null;
-    var currentROI = null;
-    var videoResolution = { width: 1920, height: 1080 };
-
-    (function init() {
-        var params = new URLSearchParams(window.location.search);
-        currentStream = params.get('stream') || 'in-low';
-        hasROI = ROI_STREAMS.indexOf(currentStream) >= 0;
-        var streamName = STREAM_NAMES[currentStream] || '未知';
-        document.title = streamName + ' - 视频监控';
-        document.getElementById('page-title').textContent = streamName;
-        document.getElementById('stream-label').textContent = streamName;
-        var navEl = document.getElementById('nav-' + currentStream);
-        if (navEl) navEl.classList.add('active');
-        if (!hasROI) {
-            document.getElementById('btn-draw-roi').style.display = 'none';
-            document.getElementById('btn-clear-roi').style.display = 'none';
-            document.getElementById('help-text').textContent = '高位摄像头无ROI功能';
-        } else {
-            document.getElementById('help-text').innerHTML =
-                'ROI仅影响推理裁剪区域,<b>上传照片仍为原始全帧</b> | ESC退出绘制模式';
-        }
-        startStream();
-        loadRoiConfig();
-        document.addEventListener('keydown', function(e) {
-            if (e.key === 'Escape' && isDrawingROI) cancelDrawROI();
-        });
-        var img = document.getElementById('video-stream');
-        img.onload = function() {
-            document.getElementById('stream-status').textContent = '🟢 在线';
-            document.getElementById('stream-status').className = 'stream-status';
-        };
-        img.onerror = function() {
-            document.getElementById('stream-status').textContent = '🔴 离线';
-            document.getElementById('stream-status').className = 'stream-status offline';
-            setTimeout(function() {
-                img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-            }, 3000);
-        };
-    })();
-
-    function startStream() {
-        document.getElementById('video-stream').src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now();
-    }
-    function refreshStream() {
-        var img = document.getElementById('video-stream');
-        img.src = '';
-        setTimeout(function() { img.src = '/api/mjpeg?stream=' + currentStream + '&t=' + Date.now(); }, 100);
-    }
-    function takeSnapshot() {
-        window.open('/api/stream?stream=' + currentStream + '&t=' + Date.now(), '_blank');
-    }
-
-    function loadRoiConfig() {
-        fetch('/api/roi/config')
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                var roi = null;
-                if (currentStream === 'in-low') roi = data.roi_in;
-                else if (currentStream === 'out-low') roi = data.roi_out;
-                if (data.resolution && data.resolution[currentStream]) {
-                    videoResolution = data.resolution[currentStream];
-                }
-                var infoEl = document.getElementById('roi-info');
-                if (!hasROI) {
-                    infoEl.innerHTML = '<span class="label">高位摄像头 — 无ROI配置</span>';
-                    return;
-                }
-                if (roi && roi.enabled) {
-                    currentROI = { x: roi.x, y: roi.y, w: roi.w, h: roi.h };
-                    infoEl.innerHTML =
-                        '<span class="label">ROI区域:</span> <span class="value">(' +
-                        roi.x + ', ' + roi.y + ') ' + roi.w + '×' + roi.h +
-                        '</span> <span class="label">| 分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height +
-                        '</span><br><span class="label">状态:</span> <span class="value" style="color:#52c41a;">✅ 已启用</span>' +
-                        ' <span class="label">| 推理裁剪,上传全帧</span>';
-                } else {
-                    currentROI = null;
-                    infoEl.innerHTML =
-                        '<span class="label">ROI状态:</span> <span class="value" style="color:#faad14;">⚪ 未启用(全帧推理)</span>' +
-                        '<br><span class="label">分辨率:</span> <span class="value">' +
-                        videoResolution.width + '×' + videoResolution.height + '</span>';
-                }
-            })
-            .catch(function(e) {
-                document.getElementById('roi-info').innerHTML =
-                    '<span class="label" style="color:#f5222d;">⚠️ 配置加载失败</span>';
-            });
-    }
-
-    function startDrawROI() {
-        if (!hasROI) return;
-        isDrawingROI = true;
-        drawStart = null;
-        var img = document.getElementById('video-stream');
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        img.src = '/api/stream?stream=' + currentStream + '&t=' + Date.now();
-        resizeCanvas();
-        canvas.style.display = 'block';
-        btn.classList.add('drawing');
-        btn.innerHTML = '🔲 绘制中...';
-        canvas.onmousedown = onDrawStart;
-        canvas.onmousemove = onDrawMove;
-        canvas.onmouseup = onDrawEnd;
-        canvas.ontouchstart = onTouchStart;
-        canvas.ontouchmove = onTouchMove;
-        canvas.ontouchend = onTouchEnd;
-    }
-
-    function cancelDrawROI() {
-        isDrawingROI = false;
-        drawStart = null;
-        var canvas = document.getElementById('roi-canvas');
-        var btn = document.getElementById('btn-draw-roi');
-        canvas.style.display = 'none';
-        canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
-        btn.classList.remove('drawing');
-        btn.innerHTML = '✏️ 绘制ROI';
-        startStream();
-    }
-
-    function finishDrawROI(rect) {
-        var canvas = document.getElementById('roi-canvas');
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        var roiX = Math.round(rect.x * scaleX);
-        var roiY = Math.round(rect.y * scaleY);
-        var roiW = Math.round(rect.w * scaleX);
-        var roiH = Math.round(rect.h * scaleY);
-        roiX = Math.max(0, Math.min(roiX, videoResolution.width - 1));
-        roiY = Math.max(0, Math.min(roiY, videoResolution.height - 1));
-        roiW = Math.min(roiW, videoResolution.width - roiX);
-        roiH = Math.min(roiH, videoResolution.height - roiY);
-        if (roiW < 50 || roiH < 50) {
-            alert('ROI区域太小(最小50×50像素),请重新绘制');
-            cancelDrawROI();
-            return;
-        }
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (confirm('确认应用' + sideName + 'ROI区域?\n坐标: (' + roiX + ', ' + roiY + ')\n尺寸: ' + roiW + '×' + roiH)) {
-            fetch('/api/roi/update', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ side: side, x: roiX, y: roiY, w: roiW, h: roiH, enabled: true })
-            })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                if (data.success) {
-                    alert('✅ ' + sideName + 'ROI已更新并保存到config.ini');
-                    loadRoiConfig();
-                } else {
-                    alert('❌ 更新失败: ' + (data.message || '未知错误'));
-                }
-            })
-            .catch(function(e) { alert('请求失败: ' + e); });
-        }
-        cancelDrawROI();
-    }
-
-    function clearROI() {
-        if (!hasROI) return;
-        var side = (currentStream === 'in-low') ? 'in' : 'out';
-        var sideName = (side === 'in') ? '进站' : '出站';
-        if (!confirm('确定清除' + sideName + 'ROI区域?\n清除后将使用全帧推理')) return;
-        fetch('/api/roi/update', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ side: side, x: 0, y: 0, w: 0, h: 0, enabled: false })
-        })
-        .then(function(r) { return r.json(); })
-        .then(function(data) {
-            if (data.success) {
-                alert('✅ ROI已清除,恢复全帧推理');
-                loadRoiConfig();
-            } else {
-                alert('❌ 清除失败: ' + (data.message || '未知错误'));
-            }
-        })
-        .catch(function(e) { alert('请求失败: ' + e); });
-    }
-
-    function resizeCanvas() {
-        var wrapper = document.getElementById('video-wrapper');
-        var canvas = document.getElementById('roi-canvas');
-        canvas.width = wrapper.clientWidth;
-        canvas.height = wrapper.clientHeight;
-    }
-
-    function getCanvasPos(e) {
-        var rect = document.getElementById('roi-canvas').getBoundingClientRect();
-        return { x: e.clientX - rect.left, y: e.clientY - rect.top };
-    }
-
-    function onDrawStart(e) { drawStart = getCanvasPos(e); }
-    function onDrawMove(e) {
-        if (!drawStart) return;
-        var canvas = document.getElementById('roi-canvas');
-        var ctx = canvas.getContext('2d');
-        var pos = getCanvasPos(e);
-        ctx.clearRect(0, 0, canvas.width, canvas.height);
-        var rx = Math.min(drawStart.x, pos.x);
-        var ry = Math.min(drawStart.y, pos.y);
-        var rw = Math.abs(pos.x - drawStart.x);
-        var rh = Math.abs(pos.y - drawStart.y);
-        ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
-        ctx.fillRect(rx, ry, rw, rh);
-        ctx.strokeStyle = '#ff0000';
-        ctx.lineWidth = 2;
-        ctx.setLineDash([6, 3]);
-        ctx.strokeRect(rx, ry, rw, rh);
-        ctx.setLineDash([]);
-        var scaleX = videoResolution.width / canvas.clientWidth;
-        var scaleY = videoResolution.height / canvas.clientHeight;
-        ctx.fillStyle = '#ff0000';
-        ctx.font = '14px sans-serif';
-        ctx.fillText(Math.round(rw * scaleX) + '×' + Math.round(rh * scaleY), rx + 4, ry + 18);
-    }
-    function onDrawEnd(e) {
-        if (!drawStart) return;
-        var pos = getCanvasPos(e);
-        var x = Math.min(drawStart.x, pos.x);
-        var y = Math.min(drawStart.y, pos.y);
-        var w = Math.abs(pos.x - drawStart.x);
-        var h = Math.abs(pos.y - drawStart.y);
-        if (w < 5 || h < 5) { cancelDrawROI(); return; }
-        finishDrawROI({ x: x, y: y, w: w, h: h });
-    }
-
-    function onTouchStart(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawStart({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchMove(e) {
-        e.preventDefault();
-        var t = e.touches[0];
-        onDrawMove({ clientX: t.clientX, clientY: t.clientY });
-    }
-    function onTouchEnd(e) {
-        e.preventDefault();
-        var t = e.changedTouches[0];
-        onDrawEnd({ clientX: t.clientX, clientY: t.clientY });
-    }
-    </script>
-</body>
-</html>

+ 0 - 751
assets/web/weight.html

@@ -1,751 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>称重管理</title>
-    <style>
-        * { box-sizing: border-box; margin: 0; padding: 0; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
-        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
-        .header h1 { font-size: 24px; margin-bottom: 5px; }
-        .nav-bar { margin-top: 12px; display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
-        .nav-link { background: rgba(255,255,255,0.2); color: white; padding: 8px 16px; border-radius: 20px; text-decoration: none; font-size: 14px; transition: background 0.2s; }
-        .nav-link:hover { background: rgba(255,255,255,0.35); }
-        .nav-link.active { background: rgba(255,255,255,0.45); font-weight: bold; }
-        .container { max-width: 1200px; margin: 20px auto; padding: 0 20px; }
-        .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
-        .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
-        .stats { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
-        .stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 20px; border-radius: 8px; min-width: 150px; }
-        .stat-card.warning { background: linear-gradient(135deg, #f5222d 0%, #cf1322 100%); }
-        .stat-card .label { font-size: 12px; opacity: 0.8; }
-        .stat-card .value { font-size: 24px; font-weight: bold; margin-top: 5px; }
-        /* fix24 v41: Tab 切换样式 */
-        .tab-bar { display: flex; gap: 0; margin-bottom: 15px; border-bottom: 2px solid #e9ecef; }
-        .tab-btn { padding: 10px 20px; background: transparent; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 14px; color: #666; transition: all 0.2s; }
-        .tab-btn:hover { color: #667eea; background: #f8f9fa; }
-        .tab-btn.active { color: #667eea; border-bottom-color: #667eea; font-weight: 600; }
-        .tab-content { display: none; }
-        .tab-content.active { display: block; }
-        /* fix24 v41: 分类查询样式 */
-        .filter-bar { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 12px; margin-bottom: 15px; }
-        .filter-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
-        .filter-row + .filter-row { margin-top: 8px; }
-        .filter-item { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 130px; }
-        .filter-item label { font-size: 12px; color: #666; font-weight: 500; }
-        .filter-item input, .filter-item select { padding: 7px 9px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; background: white; }
-        .filter-item input:focus, .filter-item select:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 2px rgba(102,126,234,0.15); }
-        .filter-buttons { flex: 0 0 auto; flex-direction: row !important; gap: 8px !important; }
-        .btn-search, .btn-reset { padding: 7px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; white-space: nowrap; }
-        .btn-search { background: #667eea; color: white; }
-        .btn-search:hover { background: #764ba2; }
-        .btn-reset { background: white; color: #666; border: 1px solid #d9d9d9; }
-        .btn-reset:hover { border-color: #667eea; color: #667eea; }
-        .table-container { overflow-x: auto; }
-        table { width: 100%; border-collapse: collapse; font-size: 13px; }
-        th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
-        th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
-        tr:hover { background: #f8f9fa; }
-        .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
-        .status-success { background: #d9f7be; color: #52c41a; }
-        .status-failed { background: #fff1f0; color: #f5222d; }
-        .status-pending { background: #f0f5ff; color: #1890ff; }
-        .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
-        .btn-retry:hover { background: #40a9ff; }
-        .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
-        .pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 0; flex-wrap: wrap; }
-        .pagination button { padding: 6px 12px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; font-size: 13px; }
-        .pagination button:hover:not(:disabled) { background: #f0f5ff; border-color: #667eea; color: #667eea; }
-        .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
-        .pagination button.active { background: #667eea; color: white; border-color: #667eea; }
-        .pagination .info { color: #666; font-size: 13px; padding: 0 4px; }
-        .pagination-bar { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; flex-wrap: wrap; gap: 10px; }
-        .pagination-left, .pagination-right { display: flex; align-items: center; gap: 5px; font-size: 13px; color: #666; }
-        .pagination-left select, .pagination-right input { padding: 5px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; width: 65px; }
-        .pagination-right input { width: 55px; }
-        .pagination-right button { padding: 5px 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
-        .pagination-right button:hover { background: #764ba2; }
-        .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
-        .loading { text-align: center; padding: 20px; color: #666; }
-        .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: #52c41a; }
-        .toast.error { background: #f5222d; }
-        .toast.show { display: block; }
-        @media (max-width: 768px) {
-            .header { padding: 14px; }
-            .header h1 { font-size: 18px; }
-            .nav-bar { gap: 6px; }
-            .nav-link { padding: 6px 12px; font-size: 12px; }
-            .container { padding: 0 10px; margin: 10px auto; }
-            .panel { padding: 12px; }
-            .panel h2 { font-size: 15px; }
-            .stats { flex-direction: column; gap: 10px; }
-            .stat-card { min-width: auto; }
-            .stat-card .value { font-size: 20px; }
-            .filter-item { min-width: 100px; }
-            .pagination-bar { flex-direction: column; align-items: stretch; }
-            .pagination-left, .pagination-right { justify-content: center; }
-            table { font-size: 11px; }
-            th, td { padding: 6px 4px; }
-            .tab-btn { padding: 8px 12px; font-size: 12px; }
-        }
-        @media (max-width: 480px) {
-            .header h1 { font-size: 16px; }
-            .nav-link { font-size: 11px; padding: 5px 10px; }
-            .filter-item { min-width: 100%; }
-        }
-    </style>
-</head>
-<body>
-    <div class="header">
-        <h1>⚖️ 称重管理系统</h1>
-        <p>Weight Management System v43.2-fix24-v43</p>
-        <div class="nav-bar">
-            <a href="/" class="nav-link">🏠 首页</a>
-            <a href="/video" class="nav-link">🎥 视频</a>
-            <a href="/weight" class="nav-link active">⚖️ 称重</a>
-            <a href="/config" class="nav-link" id="nav-config" style="display:none;">⚙️ 配置</a>
-            <a href="/locks" class="nav-link">🔒 锁定</a>
-            <a href="/help" class="nav-link" id="nav-help" style="display:none;">📖 帮助</a>
-        </div>
-    </div>
-
-    <div class="container">
-        <div class="stats">
-            <div class="stat-card">
-                <div class="label">总记录数</div>
-                <div class="value" id="stat-total">-</div>
-            </div>
-            <div class="stat-card warning">
-                <div class="label">失败记录</div>
-                <div class="value" id="stat-failed">-</div>
-            </div>
-        </div>
-
-        <div class="panel">
-            <h2>称重记录</h2>
-            
-            <!-- fix24 v41: Tab 切换 -->
-            <div class="tab-bar">
-                <button class="tab-btn active" onclick="switchTab('realtime')">📡 实时称重</button>
-                <button class="tab-btn" onclick="switchTab('history')">🔍 历史查询</button>
-                <button class="tab-btn" onclick="switchTab('sync')">🔗 同步视图</button>
-            </div>
-            
-            <!-- 实时称重 Tab -->
-            <div id="tab-realtime" class="tab-content active">
-                <div style="background:#e6f7ff;border:1px solid #91d5ff;border-radius:6px;padding:10px;margin-bottom:15px;font-size:13px;color:#0050b3;">
-                    💡 实时称重:显示最新 20 条记录,每 30 秒自动刷新
-                </div>
-                <div class="table-container">
-                    <table>
-                        <thead>
-                            <tr>
-                                <th>编号</th>
-                                <th>日期时间</th>
-                                <th>车牌号</th>
-                                <th>联单编号</th>
-                                <th>方向</th>
-                                <th>重量(kg)</th>
-                                <th>重量(吨)</th>
-                                <th>上传状态</th>
-                                <th>重试次数</th>
-                                <th>响应消息</th>
-                                <th>操作</th>
-                            </tr>
-                        </thead>
-                        <tbody id="realtime-body">
-                            <tr><td colspan="11" class="loading">加载中...</td></tr>
-                        </tbody>
-                    </table>
-                </div>
-                <div class="refresh-info" id="realtime-refresh-info">数据自动刷新间隔: 30秒</div>
-            </div>
-            
-            <!-- 历史查询 Tab -->
-            <div id="tab-history" class="tab-content">
-                <div class="filter-bar">
-                    <div class="filter-row">
-                        <div class="filter-item">
-                            <label>车牌号</label>
-                            <input type="text" id="filter-plate" placeholder="如 沪EQ3803">
-                        </div>
-                        <div class="filter-item">
-                            <label>联单编号</label>
-                            <input type="text" id="filter-tbnum" placeholder="联单编号">
-                        </div>
-                        <div class="filter-item">
-                            <label>方向</label>
-                            <select id="filter-station">
-                                <option value="0">全部</option>
-                                <option value="1">进站</option>
-                                <option value="2">出站</option>
-                            </select>
-                        </div>
-                        <div class="filter-item">
-                            <label>上传状态</label>
-                            <select id="filter-upload">
-                                <option value="0">全部</option>
-                                <option value="1">成功</option>
-                                <option value="2">失败</option>
-                            </select>
-                        </div>
-                    </div>
-                    <div class="filter-row">
-                        <div class="filter-item">
-                            <label>开始日期</label>
-                            <input type="date" id="filter-date-from">
-                        </div>
-                        <div class="filter-item">
-                            <label>结束日期</label>
-                            <input type="date" id="filter-date-to">
-                        </div>
-                        <div class="filter-item filter-buttons">
-                            <button class="btn-search" onclick="searchHistory(1)">🔍 查询</button>
-                            <button class="btn-reset" onclick="resetFilters()">重置</button>
-                        </div>
-                    </div>
-                </div>
-                <div class="table-container">
-                    <table>
-                        <thead>
-                            <tr>
-                                <th>编号</th>
-                                <th>日期时间</th>
-                                <th>车牌号</th>
-                                <th>联单编号</th>
-                                <th>方向</th>
-                                <th>重量(kg)</th>
-                                <th>重量(吨)</th>
-                                <th>上传状态</th>
-                                <th>重试次数</th>
-                                <th>响应消息</th>
-                                <th>操作</th>
-                            </tr>
-                        </thead>
-                        <tbody id="history-body">
-                            <tr><td colspan="11" style="text-align:center;padding:30px;color:#999;">请设置查询条件后点击"查询"按钮</td></tr>
-                        </tbody>
-                    </table>
-                </div>
-                <div class="pagination-bar">
-                    <div class="pagination-left">
-                        <span>每页</span>
-                        <select id="page-size" onchange="changePageSize()">
-                            <option value="20">20</option>
-                            <option value="50">50</option>
-                            <option value="100">100</option>
-                        </select>
-                        <span>条</span>
-                    </div>
-                    <div class="pagination" id="pagination"></div>
-                    <div class="pagination-right">
-                        <span>跳至</span>
-                        <input type="number" id="jump-page" min="1" value="1">
-                        <span>页</span>
-                        <button onclick="jumpToPage()">GO</button>
-                    </div>
-                </div>
-                <div class="refresh-info" id="history-refresh-info">历史查询:手动触发,不自动刷新</div>
-            </div>
-            <!-- 同步视图 Tab (fix24 v43 P2) -->
-            <div id="tab-sync" class="tab-content">
-                <div style="background:#f0f5ff;border:1px solid #adc6ff;border-radius:6px;padding:10px;margin-bottom:15px;font-size:13px;color:#1d39c4;">
-                    🔗 同步视图:基于 tb_num + 车牌号 联合匹配,展示抓拍事件与称重记录的同步状态
-                </div>
-                <!-- 同步筛选栏 -->
-                <div class="filter-bar" style="margin-bottom:15px;">
-                    <div class="filter-row">
-                    <div class="filter-item">
-                        <label>车牌号</label>
-                        <input type="text" id="sync-filter-plate" placeholder="模糊匹配">
-                    </div>
-                    <div class="filter-item">
-                        <label>联单号</label>
-                        <input type="text" id="sync-filter-tbnum" placeholder="模糊匹配">
-                    </div>
-                    <div class="filter-item">
-                        <label>起始日期</label>
-                        <input type="date" id="sync-filter-date-from">
-                    </div>
-                    <div class="filter-item">
-                        <label>截止日期</label>
-                        <input type="date" id="sync-filter-date-to">
-                    </div>
-                    <div class="filter-item">
-                        <label>站点</label>
-                        <select id="sync-filter-station">
-                            <option value="0">全部</option>
-                            <option value="1">进站</option>
-                            <option value="2">出站</option>
-                        </select>
-                    </div>
-                    <div class="filter-item">
-                        <label>同步状态</label>
-                        <select id="sync-filter-status">
-                            <option value="0">全部</option>
-                            <option value="1">已匹配</option>
-                            <option value="2">未匹配(无重量)</option>
-                            <option value="3">重量上传失败</option>
-                        </select>
-                    </div>
-                    <div class="filter-item" style="align-self:flex-end;">
-                        <button class="btn-search" onclick="searchSync(1)">🔍 查询</button>
-                        <button class="btn-reset" onclick="resetSyncFilters()" style="margin-left:5px;">重置</button>
-                    </div>
-                    </div>
-                </div>
-                <!-- 同步统计卡片 -->
-                <div class="stats" id="sync-stats-bar" style="margin-bottom:15px;">
-                    <div class="stat-card"><div class="label">同步模式</div><div class="value" id="sync-mode-display">-</div></div>
-                    <div class="stat-card"><div class="label">今日匹配率</div><div class="value" id="sync-today-rate" style="color:#52c41a;">-</div></div>
-                    <div class="stat-card"><div class="label">总匹配率</div><div class="value" id="sync-total-rate">-</div></div>
-                    <div class="stat-card"><div class="label">重量待上传</div><div class="value" id="sync-pending" style="color:#faad14;">-</div></div>
-                </div>
-                <!-- 同步结果表格 -->
-                <div class="table-container">
-                    <table >
-                        <thead>
-                            <tr>
-                                <th>抓拍时间</th>
-                                <th>车牌号</th>
-                                <th>联单号</th>
-                                <th>站点</th>
-                                <th>照片上传</th>
-                                <th>重量匹配</th>
-                                <th>重量(kg)</th>
-                                <th>重量上传状态</th>
-                            </tr>
-                        </thead>
-                        <tbody id="sync-table-body">
-                            <tr><td colspan="8" style="text-align:center;padding:30px;color:#999;">点击"查询"加载同步数据</td></tr>
-                        </tbody>
-                    </table>
-                </div>
-                <div class="pagination" id="sync-pagination"></div>
-                <div class="refresh-info" id="sync-refresh-info">同步视图:手动查询,统计每30秒自动刷新</div>
-            </div>
-        </div>
-    </div>
-
-    <div class="toast" id="toast"></div>
-
-    <script>
-    function getCookie(n){let m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}
-
-        let currentPage = 1;
-        let currentPageSize = 20;
-        let currentTotal = 0;
-        let currentTotalPages = 0;
-        let currentFilters = {};
-        let currentTab = 'realtime';
-
-        function showToast(msg, type) {
-            var t = document.getElementById('toast');
-            t.textContent = msg;
-            t.className = 'toast ' + type + ' show';
-            setTimeout(function() { t.className = 'toast'; }, 3000);
-        }
-
-        // fix24 v41: Tab 切换
-        function switchTab(tab) {
-            currentTab = tab;
-            document.querySelectorAll('.tab-btn').forEach(function(btn) { btn.classList.remove('active'); });
-            document.querySelectorAll('.tab-content').forEach(function(content) { content.classList.remove('active'); });
-            
-            if (tab === 'realtime') {
-                document.querySelector('.tab-btn:nth-child(1)').classList.add('active');
-                document.getElementById('tab-realtime').classList.add('active');
-                loadRealtimeData();
-            } else if (tab === 'history') {
-                document.querySelector('.tab-btn:nth-child(2)').classList.add('active');
-                document.getElementById('tab-history').classList.add('active');
-            } else if (tab === 'sync') {
-                document.querySelector('.tab-btn:nth-child(3)').classList.add('active');
-                document.getElementById('tab-sync').classList.add('active');
-                loadSyncStats();
-                loadSyncData(1);
-            }
-        }
-
-        function collectFilters() {
-            return {
-                plate: document.getElementById('filter-plate').value.trim(),
-                tb_num: document.getElementById('filter-tbnum').value.trim(),
-                date_from: document.getElementById('filter-date-from').value,
-                date_to: document.getElementById('filter-date-to').value,
-                station_type: parseInt(document.getElementById('filter-station').value) || 0,
-                upload_status: parseInt(document.getElementById('filter-upload').value) || 0
-            };
-        }
-
-        function resetFilters() {
-            document.getElementById('filter-plate').value = '';
-            document.getElementById('filter-tbnum').value = '';
-            document.getElementById('filter-date-from').value = '';
-            document.getElementById('filter-date-to').value = '';
-            document.getElementById('filter-station').value = '0';
-            document.getElementById('filter-upload').value = '0';
-            currentFilters = {};
-            document.getElementById('history-body').innerHTML = '<tr><td colspan="11" style="text-align:center;padding:30px;color:#999;">请设置查询条件后点击"查询"按钮</td></tr>';
-            document.getElementById('pagination').innerHTML = '';
-            document.getElementById('history-refresh-info').textContent = '历史查询:手动触发,不自动刷新';
-        }
-
-        function hasActiveFilters(f) {
-            return f.plate || f.tb_num || f.date_from || f.date_to ||
-                   f.station_type !== 0 || f.upload_status !== 0;
-        }
-
-        // fix24 v41: 实时称重数据加载
-        function loadRealtimeData() {
-            fetch('/api/weight/records?page=1&page_size=20')
-                .then(function(r) { return r.json(); })
-                .then(function(data) {
-                    currentTotal = data.total || 0;
-                    document.getElementById('stat-total').textContent = currentTotal;
-                    document.getElementById('stat-failed').textContent = data.failed;
-                    renderRecords(data, 'realtime-body');
-                    var refreshEl = document.getElementById('realtime-refresh-info');
-                    refreshEl.textContent = '数据自动刷新间隔: 30秒(共 ' + currentTotal + ' 条,显示最新 20 条)';
-                })
-                .catch(function(e) {
-                    console.error('实时称重数据加载失败:', e);
-                    document.getElementById('realtime-body').innerHTML = '<tr><td colspan="11" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
-                });
-        }
-
-        // fix24 v41: 历史查询
-        function searchHistory(page) {
-            var filters = collectFilters();
-            if (filters.date_from && filters.date_to && filters.date_from > filters.date_to) {
-                alert('开始日期不能晚于结束日期');
-                return;
-            }
-            currentFilters = filters;
-            loadHistoryData(page, filters);
-        }
-
-        function loadHistoryData(page, filters) {
-            currentPage = page;
-            currentFilters = filters || {};
-            var params = new URLSearchParams();
-            params.set('page', page);
-            params.set('page_size', currentPageSize);
-            if (currentFilters.plate) params.set('plate', currentFilters.plate);
-            if (currentFilters.tb_num) params.set('tb_num', currentFilters.tb_num);
-            if (currentFilters.date_from) params.set('date_from', currentFilters.date_from);
-            if (currentFilters.date_to) params.set('date_to', currentFilters.date_to);
-            if (currentFilters.station_type) params.set('station_type', currentFilters.station_type);
-            if (currentFilters.upload_status) params.set('upload_status', currentFilters.upload_status);
-
-            fetch('/api/weight/records?' + params.toString())
-                .then(function(r) { return r.json(); })
-                .then(function(data) {
-                    currentTotal = data.total || 0;
-                    currentTotalPages = data.total_pages || 0;
-                    document.getElementById('stat-total').textContent = currentTotal;
-                    document.getElementById('stat-failed').textContent = data.failed;
-                    renderRecords(data, 'history-body');
-                    renderPagination(data.page, currentTotalPages, currentTotal);
-                    document.getElementById('jump-page').value = data.page;
-                    document.getElementById('jump-page').max = currentTotalPages || 1;
-                    var refreshEl = document.getElementById('history-refresh-info');
-                    refreshEl.textContent = '历史查询结果:共 ' + currentTotal + ' 条(手动查询,不自动刷新)';
-                })
-                .catch(function(e) {
-                    console.error('历史查询失败:', e);
-                    document.getElementById('history-body').innerHTML = '<tr><td colspan="11" style="text-align:center;color:#f5222d;">查询失败,请重试</td></tr>';
-                });
-        }
-
-        function renderRecords(data, targetId) {
-            var html = '';
-            if (!data.records || data.records.length === 0) {
-                html = '<tr><td colspan="11" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
-            } else {
-                data.records.forEach(function(rec) {
-                    var direction = rec.station_type === 1 ? '进站' : '出站';
-                    var weightTon = rec.weight_kg > 0 ? (rec.weight_kg / 1000).toFixed(3) : '-';
-                    var weightKg = rec.weight_kg > 0 ? rec.weight_kg.toFixed(0) : '-';
-                    var statusClass, statusText;
-                    if (rec.upload_status === 1) { statusClass = 'status-success'; statusText = '成功'; }
-                    else if (rec.upload_status === 2) { statusClass = 'status-failed'; statusText = '失败'; }
-                    else { statusClass = 'status-pending'; statusText = '待上传'; }
-                    var retryBtn = rec.upload_status === 2 ? '<button class="btn-retry" onclick="retryWeight(' + rec.id + ',event)">重试</button>' : '-';
-
-                    html += '<tr>';
-                    html += '<td>' + rec.id + '</td>';
-                    html += '<td style="font-size:12px;">' + rec.create_time + '</td>';
-                    html += '<td><b>' + rec.plate_number + '</b></td>';
-                    html += '<td style="font-family:monospace;font-size:12px;">' + (rec.tb_num || '-') + '</td>';
-                    html += '<td>' + direction + '</td>';
-                    html += '<td>' + weightKg + '</td>';
-                    html += '<td>' + weightTon + '</td>';
-                    html += '<td><span class="status ' + statusClass + '">' + statusText + '</span></td>';
-                    html += '<td>' + rec.retry_count + '</td>';
-                    html += '<td style="font-size:11px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + (rec.response_msg || '') + '">' + (rec.response_msg || '-') + '</td>';
-                    html += '<td>' + retryBtn + '</td>';
-                    html += '</tr>';
-                });
-            }
-            document.getElementById(targetId).innerHTML = html;
-        }
-
-        function renderPagination(page, totalPages, total) {
-            if (!totalPages || totalPages < 1) totalPages = 1;
-            var html = '';
-            html += '<button onclick="searchHistory(1)" ' + (page <= 1 ? 'disabled' : '') + '>首页</button>';
-            html += '<button onclick="searchHistory(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>';
-
-            var start = Math.max(1, page - 2);
-            var end = Math.min(totalPages, page + 2);
-            if (start > 1) {
-                html += '<button onclick="searchHistory(1)">1</button>';
-                if (start > 2) html += '<span class="info">…</span>';
-            }
-            for (var i = start; i <= end; i++) {
-                html += '<button onclick="searchHistory(' + i + ')" class="' + (i === page ? 'active' : '') + '">' + i + '</button>';
-            }
-            if (end < totalPages) {
-                if (end < totalPages - 1) html += '<span class="info">…</span>';
-                html += '<button onclick="searchHistory(' + totalPages + ')">' + totalPages + '</button>';
-            }
-
-            html += '<button onclick="searchHistory(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>';
-            html += '<button onclick="searchHistory(' + totalPages + ')" ' + (page >= totalPages ? 'disabled' : '') + '>末页</button>';
-            html += '<span class="info">第 ' + page + '/' + totalPages + ' 页 · 共 ' + total + ' 条</span>';
-            document.getElementById('pagination').innerHTML = html;
-        }
-
-        function changePageSize() {
-            currentPageSize = parseInt(document.getElementById('page-size').value) || 20;
-            if (currentTab === 'history' && hasActiveFilters(currentFilters)) {
-                searchHistory(1);
-            }
-        }
-
-        function jumpToPage() {
-            var p = parseInt(document.getElementById('jump-page').value) || 1;
-            if (p < 1) p = 1;
-            if (currentTotalPages && p > currentTotalPages) p = currentTotalPages;
-            searchHistory(p);
-        }
-
-        function retryWeight(id, evt) {
-            if (!confirm('确定要重新上传此称重记录吗?')) return;
-
-            var btn = evt ? evt.target : event.target;
-            btn.disabled = true;
-            btn.textContent = '重试中...';
-
-            fetch('/api/weight/retry', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCookie('csrf_token') },
-                body: JSON.stringify({ id: id })
-            })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                if (data.success) {
-                    showToast('✅ ' + data.message, 'success');
-                    if (currentTab === 'realtime') loadRealtimeData();
-                    else if (hasActiveFilters(currentFilters)) loadHistoryData(currentPage, currentFilters);
-                } else {
-                    showToast('❌ ' + data.message, 'error');
-                }
-            })
-            .catch(function(e) {
-                btn.disabled = false;
-                btn.textContent = '重试';
-                showToast('请求失败: ' + e, 'error');
-            });
-        }
-
-        // 初始化
-        loadRealtimeData();
-        loadSyncStats();
-
-        // 自动刷新:实时称重 Tab + 同步统计
-        setInterval(function() {
-            if (currentTab === 'realtime') {
-                loadRealtimeData();
-            }
-            // 每30秒刷新同步统计(如果同步视图tab激活)
-            if (currentTab === 'sync') {
-                loadSyncStats();
-            }
-        }, 30000);
-
-        // 回车搜索
-        document.getElementById('filter-plate').addEventListener('keypress', function(e) {
-            if (e.key === 'Enter') searchHistory(1);
-        });
-        document.getElementById('filter-tbnum').addEventListener('keypress', function(e) {
-            if (e.key === 'Enter') searchHistory(1);
-        });
-        // fix24 v43 P2: 同步视图回车搜索
-        document.getElementById('sync-filter-plate').addEventListener('keypress', function(e) {
-            if (e.key === 'Enter') searchSync(1);
-        });
-        document.getElementById('sync-filter-tbnum').addEventListener('keypress', function(e) {
-            if (e.key === 'Enter') searchSync(1);
-        });
-
-        // 角色检查
-        fetch('/api/auth/status', {credentials:'same-origin'}).then(function(r){return r.json();}).then(function(data){
-            if(data.authenticated){
-                if(data.role >= 1){ var c=document.getElementById('nav-config'); if(c) c.style.display=''; }
-                if(data.role >= 2){ var h=document.getElementById('nav-help'); if(h) h.style.display=''; }
-            }
-        }).catch(function(){});
-        // ==================== fix24 v43 P2: 同步视图 JS ====================
-        let syncPage = 1;
-        let syncPageSize = 20;
-        let syncTotalPages = 1;
-
-        function loadSyncStats() {
-            fetch('/api/sync/stats', { credentials: 'same-origin' })
-            .then(function(r) { return r.json(); })
-            .then(function(d) {
-                document.getElementById('sync-mode-display').textContent = d.parallel_mode ? '✅ 并行模式' : '⏳ 串行模式';
-                document.getElementById('sync-mode-display').style.color = d.parallel_mode ? '#52c41a' : '#faad14';
-                document.getElementById('sync-today-rate').textContent = d.today_match_rate.toFixed(1) + '%';
-                document.getElementById('sync-total-rate').textContent = d.match_rate.toFixed(1) + '%';
-                document.getElementById('sync-pending').textContent = d.weight_pending;
-                // 同时更新首页统计卡片(如果有)
-                var el = document.getElementById('stat-sync-mode');
-                if (el) el.textContent = d.parallel_mode ? '并行' : '串行';
-                var el2 = document.getElementById('stat-match-rate');
-                if (el2) el2.textContent = d.today_match_rate.toFixed(1) + '%';
-            })
-            .catch(function() {});
-        }
-
-        function searchSync(page) {
-            syncPage = page || 1;
-            loadSyncData(syncPage);
-        }
-
-        function loadSyncData(page) {
-            syncPage = page || 1;
-            var params = new URLSearchParams({
-                page: syncPage,
-                page_size: syncPageSize,
-                plate: document.getElementById('sync-filter-plate').value.trim(),
-                tb_num: document.getElementById('sync-filter-tbnum').value.trim(),
-                date_from: document.getElementById('sync-filter-date-from').value,
-                date_to: document.getElementById('sync-filter-date-to').value,
-                station_type: document.getElementById('sync-filter-station').value,
-                sync_status: document.getElementById('sync-filter-status').value
-            });
-            fetch('/api/sync/events?' + params.toString(), { credentials: 'same-origin' })
-            .then(function(r) { return r.json(); })
-            .then(function(data) {
-                syncTotalPages = data.total_pages || 1;
-                renderSyncRecords(data.records || []);
-                renderSyncPagination(data.total, syncTotalPages);
-            })
-            .catch(function(e) {
-                document.getElementById('sync-table-body').innerHTML =
-                    '<tr><td colspan="8" style="color:#f5222d;text-align:center;padding:20px;">加载失败: ' + e + '</td></tr>';
-            });
-        }
-
-        function renderSyncRecords(records) {
-            var tbody = document.getElementById('sync-table-body');
-            if (!records.length) {
-                tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:30px;color:#999;">无匹配记录</td></tr>';
-                return;
-            }
-            var html = '';
-            for (var i = 0; i < records.length; i++) {
-                var rec = records[i];
-                var stationBadge = rec.station_type === 1
-                    ? '<span style="background:#e6f7ff;color:#0050b3;padding:2px 8px;border-radius:10px;font-size:12px;">进站</span>'
-                    : '<span style="background:#fff7e6;color:#d46b08;padding:2px 8px;border-radius:10px;font-size:12px;">出站</span>';
-                // 照片上传状态
-                var photoStatus = '';
-                if (rec.lo_upload_status === 1 && rec.hi_upload_status === 1) {
-                    photoStatus = '<span style="color:#52c41a;">✅ 双侧成功</span>';
-                } else if (rec.lo_upload_status === 2 || rec.hi_upload_status === 2) {
-                    photoStatus = '<span style="color:#f5222d;">❌ 有失败</span>';
-                } else {
-                    photoStatus = '<span style="color:#faad14;">⏳ 待上传</span>';
-                }
-                // 重量匹配列
-                var weightMatch = '';
-                if (rec.synced) {
-                    weightMatch = '<span style="color:#52c41a;font-weight:bold;">✅ 已匹配</span>';
-                } else {
-                    weightMatch = '<span style="color:#8c8c8c;">— 未匹配</span>';
-                }
-                // 重量值
-                var weightVal = rec.synced ? rec.weight_kg.toFixed(1) : '<span style="color:#bbb;">—</span>';
-                // 重量上传状态
-                var wus = '';
-                if (!rec.synced) {
-                    wus = '<span style="color:#bbb;">无记录</span>';
-                } else if (rec.weight_upload_status === 0) {
-                    wus = '<span style="color:#faad14;">⏳ 待上传</span>';
-                } else if (rec.weight_upload_status === 1) {
-                    wus = '<span style="color:#52c41a;">✅ 成功</span>';
-                } else if (rec.weight_upload_status === 2) {
-                    wus = '<span style="color:#f5222d;" title="' + (rec.weight_response_msg || '') + '">❌ 失败</span>';
-                }
-                html += '<tr>' +
-                    '<td>' + (rec.capture_time || rec.create_time) + '</td>' +
-                    '<td><strong>' + rec.plate_number + '</strong></td>' +
-                    '<td><code style="background:#f5f5f5;padding:2px 6px;border-radius:3px;">' + rec.tb_num + '</code></td>' +
-                    '<td>' + stationBadge + '</td>' +
-                    '<td>' + photoStatus + '</td>' +
-                    '<td>' + weightMatch + '</td>' +
-                    '<td>' + weightVal + '</td>' +
-                    '<td>' + wus + '</td>' +
-                    '</tr>';
-            }
-            tbody.innerHTML = html;
-        }
-
-        function renderSyncPagination(total, totalPages) {
-            var div = document.getElementById('sync-pagination');
-            if (totalPages <= 1) {
-                div.innerHTML = '<span style="color:#999;font-size:13px;">共 ' + total + ' 条记录</span>';
-                return;
-            }
-            var html = '<span style="color:#666;font-size:13px;margin-right:10px;">共 ' + total + ' 条,' + totalPages + ' 页</span>';
-            if (syncPage > 1) {
-                html += '<button onclick="searchSync(1)">首页</button>';
-                html += '<button onclick="searchSync(' + (syncPage - 1) + ')">上一页</button>';
-            }
-            var start = Math.max(1, syncPage - 2);
-            var end = Math.min(totalPages, syncPage + 2);
-            for (var p = start; p <= end; p++) {
-                if (p === syncPage) {
-                    html += '<button class="active" disabled>' + p + '</button>';
-                } else {
-                    html += '<button onclick="searchSync(' + p + ')">' + p + '</button>';
-                }
-            }
-            if (syncPage < totalPages) {
-                html += '<button onclick="searchSync(' + (syncPage + 1) + ')">下一页</button>';
-                html += '<button onclick="searchSync(' + totalPages + ')">末页</button>';
-            }
-            div.innerHTML = html;
-        }
-
-        function resetSyncFilters() {
-            document.getElementById('sync-filter-plate').value = '';
-            document.getElementById('sync-filter-tbnum').value = '';
-            document.getElementById('sync-filter-date-from').value = '';
-            document.getElementById('sync-filter-date-to').value = '';
-            document.getElementById('sync-filter-station').value = '0';
-            document.getElementById('sync-filter-status').value = '0';
-            searchSync(1);
-        }
-
-    </script>
-</body>
-</html>

+ 0 - 431
config/config一号门.ini

@@ -1,431 +0,0 @@
-[sync]
-# 开启并行模式enabled=1,获取联单编号后,上传照片和重量并行同时上传
-enabled= 1
-#带时间戳防串车,交替锁定保证 20 秒内不可能有第二辆车
-weight_snapshot_max_age_sec = 20
-#重量上报API超时(秒),最小5
-reporter_timeout_sec= 15
-#重量上传最大重试次数,最小0
-weight_retry_max= 5
-#重量上传重试间隔(秒),最小1
-weight_retry_interval_sec= 10
-#同联单编号去重窗口(秒),最小10。同一 tb_num 在此时间内只触发一次重量读取
-tb_num_dedup_sec= 120
-
-[server]
-#配置文件等号左右可以有空格也可以没有
-#1、192.168.3.21
-#ip=http://192.168.3.21:8000  
-#2、8.210.227.192
-#ip= http://0.0.0.0:8080
-ip= https://0.0.0.0:8080
-
-#3、 Ubuntu 172.20.131.63       
-#ip=http://172.20.131.63:8000   
-# opt/openAI/project/003.PlateRecAPP/build $ 
-#./PlateRecApp > ./PlateRecApp.log &                     
-[ssl]
-enabled= 1
-cert_path= /opt/openAI/project/003.PlateRecAPP/ssl/fullchain.pem
-key_path= /opt/openAI/project/003.PlateRecAPP/ssl/privkey.pem
-[config]
-
-app_api = https://ets.lhsr.sh.gov.cn/shcws/api/cws-plstb/threeBill/
-###2、新项目需要配置密钥
-app_key = 12c78107-d181-4ed4-a9aa-3259d1bb505c
-app_secret = 1LpMMHoS7SEqmKVZfAUR5IFb5LzjGsGRixXVeMwK7YAPK8zATcuBNIxoeWXfG6kz
-
-###3、新项目需要配置项目信息
-#project_name = 上海轨道交通市域线嘉闵线工程JMSG-1标(主体)
-project_name = JMSG-1标(主体)
-point_number = GD202400176
-throughway = 一号门
-
-###4、新项目需要配置4个摄像头
-#摄像头设置h.264 去掉smart264,编码,400万像素设置 1280*720P
-#摄像头设置h.264 去掉smart264,编码,400万像素设置 1280*720P
-#设置RTSP视频流的URL
-#正面进入,正面出去
-#rtsp_url_front_in  = rtsp://admin:a123456789@192.168.0.5
-#rtsp_url_front_out = rtsp://admin:a123456789@192.168.0.7
-#侧面摄像头
-#rtsp_url_side_in   = rtsp://admin:a123456789@192.168.0.6
-#rtsp_url_side_out  = rtsp://admin:a123456789@192.168.0.8
-#低位摄像头进站
-rtsp_url_front_in  = rtsp://admin:a123456789@192.168.0.4
-#低位摄像头出站
-rtsp_url_front_out = rtsp://admin:a123456789@192.168.0.2/stream1
-#2
-#高位摄像头进站
-rtsp_url_side_in   = rtsp://admin:a123456789@192.168.0.3
-#高位摄像头出站
-rtsp_url_side_out  = rtsp://admin:a123456789@192.168.0.9
-
-#####################################################
-#1	TRUE  调试模式 	 
-#0	FLASE 正式运行 debug=1
-#1调试模式 
-#0正式运行					***********************
-DEBUG_LOG= 0
-#1
-
-###5、新项目测试配置,正式上传需要修改
-#此处正式运行后续,必须修改为0	*********************
-#1、httpsTest调试测试模式上传	
-#0、httpsT正式运行上传
-TestFlag= 0
-#系统校验*************
-CRC= 20241024999
-
-#####根据实际渣土车进出时间调整。5分钟内上传车牌号,获取联单编号、车牌号5分钟内不重复上传
-TIME_WINDOW = 4
-MAX_PHOTO_GROUPS= 2
-PLATE_CONFIDENCE_THRESHOLD= 0.9745f
-#9845f
-PLATE_LOG_THRESHOLD= 0.80f
-###站点类型 1:工地,2:码头,3:回填点 消纳方 
-#wType= 3 卸点 卸点发送参数不一样 ********
-#wType= 1 工地
-wType= 1
-
-# 1=启用交替锁定,0=禁用  注意测试模式:0=禁用XXXXX
-AlternatingMerge= 1
-#必须先进站5   300秒后,再出站抓拍照片称重上传
-in_out_interval= 4
-
-
-# === ROI配置 (v43新增) ===
-# 进站ROI区域X偏移
-roi_in_x= 314
-# 进站ROI区域Y偏移
-roi_in_y= 137
-# 进站ROI区域宽度(0=全帧)          
-roi_in_w= 762
-  # 进站ROI区域高度(0=全帧)   
-roi_in_h= 583
-  # 进站ROI启用(1=启用,0=禁用)
-roi_in_enabled= 1
-   # 出站ROI区域X偏移
-roi_out_x= 251
- # 出站ROI区域Y偏移
-roi_out_y= 83
- # 出站ROI区域宽度
-roi_out_w= 955
-  # 出站ROI区域高度      
-roi_out_h= 635
- # 出站ROI启用
-roi_out_enabled= 1
-# ROI调试模式(保存裁剪帧)
-roi_debug_enabled= 0
-
-# === 解码配置 (v43新增) ===
-# auto/drm/soft
-hw_decode_mode= auto
-hw_decode_device= /dev/dri/renderD128
-# tcp/udp
-rtsp_transport= tcp
-#####################################################
-
-#####################################################
-#####################################################
-#####################################################
-# 可选:通道级高级配置(不写则使用自动检测)
-# channel_0_decoder=hevc      # 强制指定解码器(auto/hevc/h264)
-# channel_0_hardware_accel=true # 强制启用/禁用硬件加速
-# channel_1_decoder=hevc
-# channel_1_hardware_accel=true
-# channel_2_decoder=hevc
-# channel_2_hardware_accel=true
-# channel_3_decoder=h264
-# channel_3_hardware_accel=false
-#3、读取config.ini PRECISION_INT8可以配置
-#  INT8
-#PRECISION_INT8=true
-#  FP32
-#PRECISION_INT8= false
-# true
-#false
-#精度模式: " << (USE_PRECISION == HyperLPREngine::PRECISION_INT8 ? "INT8" : "FP32") << std::endl;
-#;改成1 每秒识别25帧
-#;改成2 每2帧识别一次,每秒识别12.5帧
-#RECOGNIZE_INTERVAL = 2
-# 1
-
-# =============================================
-# 运行模式:只需要修改这一个参数即可切换模式
-# low_power: 低功耗模式(4路总CPU≈25%)
-# high_performance: 高性能模式(4路总CPU≈60%)
-# =============================================
-RUN_MODE= high_performance
-
-# =============================================
-# 以下参数可选,不写则使用对应模式的默认值
-# 单独配置会覆盖模式默认值,用于微调
-# =============================================
-
-# 核心识别参数
-# 低功耗默认4,高性能默认2
-RECOGNIZE_INTERVAL= 3
-#false FP32 # true 必须开启INT8精度
-PRECISION_INT8= false
-# DEBUG_LOG=0                # 强制关闭调试日志
-
-# 性能调优参数
- # 低功耗默认4,高性能默认10
-MAX_INFERENCE_FPS= 10
-# 低功耗默认3,高性能默认2
-MATCH_THRESHOLD= 2
-# 低功耗默认2,高性能默认4        
-MAX_QUEUE_SIZE= 4
-# 低功耗默认1,高性能默认2     
-INFERENCE_THREADS= 2
-# 低功耗默认low, medium 高性能默认high
-DETECT_LEVEL= high
-
-###########################################
-#照片容量阈值
-PhotoMaxCapacityMB= 1024
-#日志保留天数
-LogRetentionDays= 30
-
-[LED]
-### 3、从config.ini配置文件读取进站led控制卡IP、port,出站LED控制卡IP、port 
-### 主要功能:
-#### 1、进站低位照片、进站高位照片,上传成功后,再发送到LED控制卡滚动内容:沪FQ8602,三联单号:SHCWS20260410000314038,成功。
-#### 2、出站低位照片、出站高位照片,上传成功后,再发送到LED控制卡滚动内容:沪FQ8602,三联单号:SHCWS20260410000314039,成功。
-IN_LED_IP= 192.168.112.95
-IN_LED_PORT= 6666
-OUT_LED_IP= 192.168.112.95
-OUT_LED_PORT= 6666
-                    
-[MQTT]
-# fix27: MQTT消息独立开关 1=启用 0=禁用
-MQTT_ENABLED = 1
-#MQTT 连接配置
-MQTT_HOST = broker.emqx.io
-MQTT_PORT  = 1883
-MQTT_USER = mqtt
-MQTT_PASS = mqtt123Q
-MQTT_TOPIC = p/jmd1
-MQTT_CLIENT_ID= cpp_mqtt_publisher2
-
-#LOG_FILE    = /opt/openAI/project/003.PlateRecAPP/PlateRecApp.log
-LOG_FILE    = PlateRecApp.log
-#OFFSET_FILE = /var/lib/plate_log2mqtt/offset.dat
-#OFFSET_FILE    = /opt/openAI/project/003.PlateRecAPP/offset.dat
-OFFSET_FILE    = offset.dat
-
-PLATE_COLOR  = 黄色
-VEHICLE_TYPE = 渣土车
-[Test]
-createBill= createBillTest?app_key=
-uploadImg= uploadImgTest?app_key=
-onlineStatus= onlineStatusTest?app_key=
-createWsiteVehicleWeigh= createWeighTest?app_key=
-
-[Release]
-uploadImg= uploadImg?app_key=
-onlineStatus= onlineStatus?app_key=
-
-#wType= 1
-createBill= createBill?app_key=
-createWsiteVehicleWeigh= createWsiteVehicleWeigh?app_key=
-
-#wType= 3  #createDsiteVehicleWeigh= createDsiteVehicleWeigh?app_key=
-#createBill=consumeBill?app_key=
-#createWsiteVehicleWeigh= createDsiteVehicleWeigh?app_key=
-
-##7、新项目渣土车修改
-[weight]
-#flagWeight=0,不上传称重数据
-#flagWeight=1,上传称重数据
-flagWeight= 1
-
-#现有的配置...
-#称重服务器配置
-weight_server_ip= 192.168.0.171
-weight_server_port= 8887
-#重量阈值配置(吨)
-#进站重量 5.0  5000kg=5吨  实际调整为10000kg
-weight_threshold_in= 500
-#出站重量20.0 20000kg=20吨
-weight_threshold_out= 500
-#默认进站车辆重量 5000kg 注意单位转换成吨上传
-default_entry_weight= 500.0
-#默认出站车辆重量 20000kg 注意单位转换成吨上传
-default_exit_weight= 2000
-#重量检测时间(秒)15  180
-weight_detection_time= 8
-#TCP连接超时(秒)
-tcp_connect_timeout= 15
-
-
-########################################3
-# 上传配置
-UPLOAD_INTERVAL_SECONDS= 30
-MAX_UPLOAD_RETRIES= 3
-RETRY_DELAY_MS= 2000
-
-# 称重策略配置       注意:上传公斤(kg)转换为吨(t)
-# 每次上传保留前5个有效数据
-CANDIDATE_DATA_COUNT= 5
-# 稳定判断需要连续3个样本
-STABLE_SAMPLE_COUNT= 3
-# 重量波动≤0.5kg 500kg视为稳定
-STABLE_THRESHOLD_KG= 500
-# 最大重量60吨  MAX_WEIGHT=100000.0 100000kg=100吨
-MAX_WEIGHT= 60000.0
-
-# v44 P0: 预检测锁定阈值(kg)
-# 重型车辆为主: 9999 | 有轻载进站: 2000~3000 | 0=禁用
-PRELOCK_THRESHOLD_KG = 9999
-# v44 P0: 连续N个稳定样本才锁定(每个200ms,5=1秒)
-PRELOCK_STABLE_SAMPLES = 5
-
-[feishu]
-# fix27: 飞书消息独立开关 1=启用飞书消息推送 0=禁用
-ENABLED = 1
-# 0=启用飞书告警,1=禁用飞书告警
-WarningSigns= 1
-
-###1、新项目需要配置飞书消息ID信息,老的过度
-##// ======飞书配置 
-APP_ID  = cli_a9341fc7df219cb1
-APP_SECRET = BBp1vMaCrGe2QsKnmB72TdykFLoN5tWg
-#CHAT_ID =oc_8a2f600f72f9582d6f4d2bd01c854de8
-CHAT_ID = oc_d11a3088c7327121e8c79894d6558441
-TIMEOUT_SECOND = 10L
-##// =======
-
-##
-
-##1登录
-[auth]
-# 认证功能开关(0=关闭, 1=开启)
-enabled= 1
-# 认证数据库路径
-db_path= data/auth.db
-#upload_records_auth.db
-# Session 空闲超时(秒),默认 1800 = 30分钟
-session_idle_timeout= 1800
-# Session 绝对超时(秒),默认 28800 = 8小时
-session_max_timeout= 28800
-# 记住登录状态时 Session 超时(秒),默认 604800 = 7天
-session_remember_timeout= 604800
-# bcrypt 哈希成本因子(10~14,越大越慢越安全)
-bcrypt_cost= 12
-# 最大登录失败次数(超过则锁定账户)
-max_failed_attempts= 5
-# 账户锁定时间(秒),默认 900 = 15分钟
-lockout_duration= 900
-# IP 限流窗口内最大尝试次数
-rate_limit_max= 5
-# IP 限流窗口时长(秒),默认 300 = 5分钟
-rate_limit_window= 300
-
-
-[system]
-#2 系统监控刷新间隔(秒)
-monitor_interval= 2
-# 温度告警阈值(°C)
-temp_alert_threshold= 70
-# CPU告警阈值(%)
-cpu_alert_threshold= 90
-# SQLite数据库路径
-db_path= data/system_metrics.db
-# 全量数据保留天数
-history_retention_days= 7
-# 聚合数据保留天数
-aggregation_retention_days= 365
-# 批量写入间隔(秒)
-# 把原来的:
-# db_write_interval= 30
-# 改为:
-flush_time= 23:20
-
-[log]
-###################
-#enabled=1 启用 内存写入
-#enabled=0 禁用 直接写入PlateRecApp.log
-enabled= 1
-#日志文件路径(相对或绝对)
-log_file= PlateRecApp.log
-# 内存缓冲区上限 500MB #3 日志缓冲区大小(MB)
-buffer_size_mb= 20
-# 每天定时刷盘时间    日志写回时间(HH:MM)
-flush_time= 23:20
-# 日志文件保留天数   
-retention_days= 30
-# 是否捕获stdout   是否重定向 stdout/stderr     
-redirect_stdout= 1
-# 程序退出时自动刷盘     
-auto_flush_on_exit= 1
-
-
-[terminal]
-ttyd_port= 7681
-ttyd_credential= admin:Zhongjin@Ttyd2026!
-ttyd_max_clients= 3
-ssh_keys_path= /root/.ssh/authorized_keys
-#4 Web SSH 管理页面开关(0=隐藏, 1=显示)
-# 实际终端由 ttyd 独立提供,这里只控制 Web 管理页面入口
-#enabled=1
-# ttyd 访问地址(显示在 Web 页面上的链接)
-#ttyd_url=http://mqtt.zhonjin.com:40964
-
-[frpc]
-#5 frpc 配置文件路径
-config_path= /opt/openAI/frp/frpc.toml
-# 备份文件路径
-backup_path= /opt/openAI/frp/frpc.toml.bak
-# 受保护隧道列表(逗号分隔)
-protected_tunnels= ssd_40960_ssh_tcp_40960_22,ssd_40960_ttyd_tcp_40964_7681
-#plate_rec_app
-# 重启后等待检测时间(秒)
-restart_check_timeout= 5
-# 重启失败自动回滚(0=关闭, 1=开启)
-auto_rollback= 1
-# Web 管理页面开关
-web_enabled= 1
-# frpc admin API 地址(仅本机访问)
-admin_addr= 127.0.0.1
-admin_port= 7400
-admin_user= admin
-admin_password= Zhongjin@Frpc2026
-
-
-#8
-#增加测试车牌
-[PlateRec]
-Total= 31
-car[0]=沪FQ8077
-car[10]=沪FR6999
-car[11]=沪FQ0922
-car[12]=沪FP6182
-car[13]=沪FK8286
-car[14]=沪FR0568
-car[15]=沪FQ6059
-car[16]=沪FQ9292
-car[17]=沪FR8793
-car[18]=沪FR1339
-car[19]=沪FR7920
-car[1]=沪FQ3385
-car[20]=沪FR2609
-car[21]=沪FQ2726
-car[22]=沪FQ1230
-car[23]=沪FR1003
-car[24]=沪FR0883
-car[25]=沪FR9967
-car[26]=沪FL2989
-car[27]=沪FQ3956
-car[28]=沪FQ6687
-car[29]=沪FR7082
-car[2]=沪FQ9359
-car[30]=沪EQ3803
-car[3]=沪FR0809
-car[4]=沪FQ7108
-car[5]=沪FR3259
-car[6]=沪FQ9950
-car[7]=沪FQ7692
-car[8]=沪FR9873
-car[9]=沪FR1172

+ 0 - 431
config/config二号门.ini

@@ -1,431 +0,0 @@
-[sync]
-# 开启并行模式enabled=1,获取联单编号后,上传照片和重量并行同时上传
-enabled= 1
-#带时间戳防串车,交替锁定保证 20 秒内不可能有第二辆车
-weight_snapshot_max_age_sec = 20
-#重量上报API超时(秒),最小5
-reporter_timeout_sec= 15
-#重量上传最大重试次数,最小0
-weight_retry_max= 5
-#重量上传重试间隔(秒),最小1
-weight_retry_interval_sec= 10
-#同联单编号去重窗口(秒),最小10。同一 tb_num 在此时间内只触发一次重量读取
-tb_num_dedup_sec= 120
-
-[server]
-#配置文件等号左右可以有空格也可以没有
-#1、192.168.3.21
-#ip=http://192.168.3.21:8000  
-#2、8.210.227.192
-#ip= http://0.0.0.0:8080
-ip= https://0.0.0.0:8080
-
-#3、 Ubuntu 172.20.131.63       
-#ip=http://172.20.131.63:8000   
-# opt/openAI/project/003.PlateRecAPP/build $ 
-#./PlateRecApp > ./PlateRecApp.log &                     
-[ssl]
-enabled= 1
-cert_path= /opt/openAI/project/003.PlateRecAPP/ssl/fullchain.pem
-key_path= /opt/openAI/project/003.PlateRecAPP/ssl/privkey.pem
-[config]
-
-app_api = https://ets.lhsr.sh.gov.cn/shcws/api/cws-plstb/threeBill/
-###2、新项目需要配置密钥
-app_key = 12c78107-d181-4ed4-a9aa-3259d1bb505c
-app_secret = 1LpMMHoS7SEqmKVZfAUR5IFb5LzjGsGRixXVeMwK7YAPK8zATcuBNIxoeWXfG6kz
-
-###3、新项目需要配置项目信息
-#project_name = 上海轨道交通市域线嘉闵线工程JMSG-1标(主体)
-project_name = JMSG-1标(主体)
-point_number = GD202400176
-throughway = 一号门
-
-###4、新项目需要配置4个摄像头
-#摄像头设置h.264 去掉smart264,编码,400万像素设置 1280*720P
-#摄像头设置h.264 去掉smart264,编码,400万像素设置 1280*720P
-#设置RTSP视频流的URL
-#正面进入,正面出去
-#rtsp_url_front_in  = rtsp://admin:a123456789@192.168.0.5
-#rtsp_url_front_out = rtsp://admin:a123456789@192.168.0.7
-#侧面摄像头
-#rtsp_url_side_in   = rtsp://admin:a123456789@192.168.0.6
-#rtsp_url_side_out  = rtsp://admin:a123456789@192.168.0.8
-#低位摄像头进站
-rtsp_url_front_in  = rtsp://admin:a123456789@192.168.0.4
-#低位摄像头出站
-rtsp_url_front_out = rtsp://admin:a123456789@192.168.0.2/stream1
-#2
-#高位摄像头进站
-rtsp_url_side_in   = rtsp://admin:a123456789@192.168.0.3
-#高位摄像头出站
-rtsp_url_side_out  = rtsp://admin:a123456789@192.168.0.9
-
-#####################################################
-#1	TRUE  调试模式 	 
-#0	FLASE 正式运行 debug=1
-#1调试模式 
-#0正式运行					***********************
-DEBUG_LOG= 0
-#1
-
-###5、新项目测试配置,正式上传需要修改
-#此处正式运行后续,必须修改为0	*********************
-#1、httpsTest调试测试模式上传	
-#0、httpsT正式运行上传
-TestFlag= 0
-#系统校验*************
-CRC= 20241024999
-
-#####根据实际渣土车进出时间调整。5分钟内上传车牌号,获取联单编号、车牌号5分钟内不重复上传
-TIME_WINDOW = 4
-MAX_PHOTO_GROUPS= 2
-PLATE_CONFIDENCE_THRESHOLD= 0.9745f
-#9845f
-PLATE_LOG_THRESHOLD= 0.80f
-###站点类型 1:工地,2:码头,3:回填点 消纳方 
-#wType= 3 卸点 卸点发送参数不一样 ********
-#wType= 1 工地
-wType= 1
-
-# 1=启用交替锁定,0=禁用  注意测试模式:0=禁用XXXXX
-AlternatingMerge= 1
-#必须先进站5   300秒后,再出站抓拍照片称重上传
-in_out_interval= 4
-
-
-# === ROI配置 (v43新增) ===
-# 进站ROI区域X偏移
-roi_in_x= 314
-# 进站ROI区域Y偏移
-roi_in_y= 137
-# 进站ROI区域宽度(0=全帧)          
-roi_in_w= 762
-  # 进站ROI区域高度(0=全帧)   
-roi_in_h= 583
-  # 进站ROI启用(1=启用,0=禁用)
-roi_in_enabled= 1
-   # 出站ROI区域X偏移
-roi_out_x= 251
- # 出站ROI区域Y偏移
-roi_out_y= 83
- # 出站ROI区域宽度
-roi_out_w= 955
-  # 出站ROI区域高度      
-roi_out_h= 635
- # 出站ROI启用
-roi_out_enabled= 1
-# ROI调试模式(保存裁剪帧)
-roi_debug_enabled= 0
-
-# === 解码配置 (v43新增) ===
-# auto/drm/soft
-hw_decode_mode= auto
-hw_decode_device= /dev/dri/renderD128
-# tcp/udp
-rtsp_transport= tcp
-#####################################################
-
-#####################################################
-#####################################################
-#####################################################
-# 可选:通道级高级配置(不写则使用自动检测)
-# channel_0_decoder=hevc      # 强制指定解码器(auto/hevc/h264)
-# channel_0_hardware_accel=true # 强制启用/禁用硬件加速
-# channel_1_decoder=hevc
-# channel_1_hardware_accel=true
-# channel_2_decoder=hevc
-# channel_2_hardware_accel=true
-# channel_3_decoder=h264
-# channel_3_hardware_accel=false
-#3、读取config.ini PRECISION_INT8可以配置
-#  INT8
-#PRECISION_INT8=true
-#  FP32
-#PRECISION_INT8= false
-# true
-#false
-#精度模式: " << (USE_PRECISION == HyperLPREngine::PRECISION_INT8 ? "INT8" : "FP32") << std::endl;
-#;改成1 每秒识别25帧
-#;改成2 每2帧识别一次,每秒识别12.5帧
-#RECOGNIZE_INTERVAL = 2
-# 1
-
-# =============================================
-# 运行模式:只需要修改这一个参数即可切换模式
-# low_power: 低功耗模式(4路总CPU≈25%)
-# high_performance: 高性能模式(4路总CPU≈60%)
-# =============================================
-RUN_MODE= high_performance
-
-# =============================================
-# 以下参数可选,不写则使用对应模式的默认值
-# 单独配置会覆盖模式默认值,用于微调
-# =============================================
-
-# 核心识别参数
-# 低功耗默认4,高性能默认2
-RECOGNIZE_INTERVAL= 3
-#false FP32 # true 必须开启INT8精度
-PRECISION_INT8= false
-# DEBUG_LOG=0                # 强制关闭调试日志
-
-# 性能调优参数
- # 低功耗默认4,高性能默认10
-MAX_INFERENCE_FPS= 10
-# 低功耗默认3,高性能默认2
-MATCH_THRESHOLD= 2
-# 低功耗默认2,高性能默认4        
-MAX_QUEUE_SIZE= 4
-# 低功耗默认1,高性能默认2     
-INFERENCE_THREADS= 2
-# 低功耗默认low, medium 高性能默认high
-DETECT_LEVEL= high
-
-###########################################
-#照片容量阈值
-PhotoMaxCapacityMB= 1024
-#日志保留天数
-LogRetentionDays= 30
-
-[LED]
-### 3、从config.ini配置文件读取进站led控制卡IP、port,出站LED控制卡IP、port 
-### 主要功能:
-#### 1、进站低位照片、进站高位照片,上传成功后,再发送到LED控制卡滚动内容:沪FQ8602,三联单号:SHCWS20260410000314038,成功。
-#### 2、出站低位照片、出站高位照片,上传成功后,再发送到LED控制卡滚动内容:沪FQ8602,三联单号:SHCWS20260410000314039,成功。
-IN_LED_IP= 192.168.112.95
-IN_LED_PORT= 6666
-OUT_LED_IP= 192.168.112.95
-OUT_LED_PORT= 6666
-                    
-[MQTT]
-# fix27: MQTT消息独立开关 1=启用 0=禁用
-MQTT_ENABLED = 1
-#MQTT 连接配置
-MQTT_HOST = broker.emqx.io
-MQTT_PORT  = 1883
-MQTT_USER = mqtt
-MQTT_PASS = mqtt123Q
-MQTT_TOPIC = p/jmd1
-MQTT_CLIENT_ID= cpp_mqtt_publisher2
-
-#LOG_FILE    = /opt/openAI/project/003.PlateRecAPP/PlateRecApp.log
-LOG_FILE    = PlateRecApp.log
-#OFFSET_FILE = /var/lib/plate_log2mqtt/offset.dat
-#OFFSET_FILE    = /opt/openAI/project/003.PlateRecAPP/offset.dat
-OFFSET_FILE    = offset.dat
-
-PLATE_COLOR  = 黄色
-VEHICLE_TYPE = 渣土车
-[Test]
-createBill= createBillTest?app_key=
-uploadImg= uploadImgTest?app_key=
-onlineStatus= onlineStatusTest?app_key=
-createWsiteVehicleWeigh= createWeighTest?app_key=
-
-[Release]
-uploadImg= uploadImg?app_key=
-onlineStatus= onlineStatus?app_key=
-
-#wType= 1
-createBill= createBill?app_key=
-createWsiteVehicleWeigh= createWsiteVehicleWeigh?app_key=
-
-#wType= 3  #createDsiteVehicleWeigh= createDsiteVehicleWeigh?app_key=
-#createBill=consumeBill?app_key=
-#createWsiteVehicleWeigh= createDsiteVehicleWeigh?app_key=
-
-##7、新项目渣土车修改
-[weight]
-#flagWeight=0,不上传称重数据
-#flagWeight=1,上传称重数据
-flagWeight= 1
-
-#现有的配置...
-#称重服务器配置
-weight_server_ip= 192.168.0.171
-weight_server_port= 8887
-#重量阈值配置(吨)
-#进站重量 5.0  5000kg=5吨  实际调整为10000kg
-weight_threshold_in= 500
-#出站重量20.0 20000kg=20吨
-weight_threshold_out= 500
-#默认进站车辆重量 5000kg 注意单位转换成吨上传
-default_entry_weight= 500.0
-#默认出站车辆重量 20000kg 注意单位转换成吨上传
-default_exit_weight= 2000
-#重量检测时间(秒)15  180
-weight_detection_time= 8
-#TCP连接超时(秒)
-tcp_connect_timeout= 15
-
-
-########################################3
-# 上传配置
-UPLOAD_INTERVAL_SECONDS= 30
-MAX_UPLOAD_RETRIES= 3
-RETRY_DELAY_MS= 2000
-
-# 称重策略配置       注意:上传公斤(kg)转换为吨(t)
-# 每次上传保留前5个有效数据
-CANDIDATE_DATA_COUNT= 5
-# 稳定判断需要连续3个样本
-STABLE_SAMPLE_COUNT= 3
-# 重量波动≤0.5kg 500kg视为稳定
-STABLE_THRESHOLD_KG= 500
-# 最大重量60吨  MAX_WEIGHT=100000.0 100000kg=100吨
-MAX_WEIGHT= 60000.0
-
-# v44 P0: 预检测锁定阈值(kg)
-# 重型车辆为主: 9999 | 有轻载进站: 2000~3000 | 0=禁用
-PRELOCK_THRESHOLD_KG = 9999
-# v44 P0: 连续N个稳定样本才锁定(每个200ms,5=1秒)
-PRELOCK_STABLE_SAMPLES = 5
-
-[feishu]
-# fix27: 飞书消息独立开关 1=启用飞书消息推送 0=禁用
-ENABLED = 1
-# 0=启用飞书告警,1=禁用飞书告警
-WarningSigns= 1
-
-###1、新项目需要配置飞书消息ID信息,老的过度
-##// ======飞书配置 
-APP_ID  = cli_a9341fc7df219cb1
-APP_SECRET = BBp1vMaCrGe2QsKnmB72TdykFLoN5tWg
-#CHAT_ID =oc_8a2f600f72f9582d6f4d2bd01c854de8
-CHAT_ID = oc_d11a3088c7327121e8c79894d6558441
-TIMEOUT_SECOND = 10L
-##// =======
-
-##
-
-##1登录
-[auth]
-# 认证功能开关(0=关闭, 1=开启)
-enabled= 1
-# 认证数据库路径
-db_path= data/auth.db
-#upload_records_auth.db
-# Session 空闲超时(秒),默认 1800 = 30分钟
-session_idle_timeout= 1800
-# Session 绝对超时(秒),默认 28800 = 8小时
-session_max_timeout= 28800
-# 记住登录状态时 Session 超时(秒),默认 604800 = 7天
-session_remember_timeout= 604800
-# bcrypt 哈希成本因子(10~14,越大越慢越安全)
-bcrypt_cost= 12
-# 最大登录失败次数(超过则锁定账户)
-max_failed_attempts= 5
-# 账户锁定时间(秒),默认 900 = 15分钟
-lockout_duration= 900
-# IP 限流窗口内最大尝试次数
-rate_limit_max= 5
-# IP 限流窗口时长(秒),默认 300 = 5分钟
-rate_limit_window= 300
-
-
-[system]
-#2 系统监控刷新间隔(秒)
-monitor_interval= 2
-# 温度告警阈值(°C)
-temp_alert_threshold= 70
-# CPU告警阈值(%)
-cpu_alert_threshold= 90
-# SQLite数据库路径
-db_path= data/system_metrics.db
-# 全量数据保留天数
-history_retention_days= 7
-# 聚合数据保留天数
-aggregation_retention_days= 365
-# 批量写入间隔(秒)
-# 把原来的:
-# db_write_interval= 30
-# 改为:
-flush_time= 23:20
-
-[log]
-###################
-#enabled=1 启用 内存写入
-#enabled=0 禁用 直接写入PlateRecApp.log
-enabled= 1
-#日志文件路径(相对或绝对)
-log_file= PlateRecApp.log
-# 内存缓冲区上限 500MB #3 日志缓冲区大小(MB)
-buffer_size_mb= 20
-# 每天定时刷盘时间    日志写回时间(HH:MM)
-flush_time= 23:20
-# 日志文件保留天数   
-retention_days= 30
-# 是否捕获stdout   是否重定向 stdout/stderr     
-redirect_stdout= 1
-# 程序退出时自动刷盘     
-auto_flush_on_exit= 1
-
-
-[terminal]
-ttyd_port= 7681
-ttyd_credential= admin:Zhongjin@Ttyd2026!
-ttyd_max_clients= 3
-ssh_keys_path= /root/.ssh/authorized_keys
-#4 Web SSH 管理页面开关(0=隐藏, 1=显示)
-# 实际终端由 ttyd 独立提供,这里只控制 Web 管理页面入口
-#enabled=1
-# ttyd 访问地址(显示在 Web 页面上的链接)
-#ttyd_url=http://mqtt.zhonjin.com:40964
-
-[frpc]
-#5 frpc 配置文件路径
-config_path= /opt/openAI/frp/frpc.toml
-# 备份文件路径
-backup_path= /opt/openAI/frp/frpc.toml.bak
-# 受保护隧道列表(逗号分隔)
-protected_tunnels= ssd_40960_ssh_tcp_40960_22,ssd_40960_ttyd_tcp_40964_7681
-#plate_rec_app
-# 重启后等待检测时间(秒)
-restart_check_timeout= 5
-# 重启失败自动回滚(0=关闭, 1=开启)
-auto_rollback= 1
-# Web 管理页面开关
-web_enabled= 1
-# frpc admin API 地址(仅本机访问)
-admin_addr= 127.0.0.1
-admin_port= 7400
-admin_user= admin
-admin_password= Zhongjin@Frpc2026
-
-
-#8
-#增加测试车牌
-[PlateRec]
-Total= 31
-car[0]=沪FQ8077
-car[10]=沪FR6999
-car[11]=沪FQ0922
-car[12]=沪FP6182
-car[13]=沪FK8286
-car[14]=沪FR0568
-car[15]=沪FQ6059
-car[16]=沪FQ9292
-car[17]=沪FR8793
-car[18]=沪FR1339
-car[19]=沪FR7920
-car[1]=沪FQ3385
-car[20]=沪FR2609
-car[21]=沪FQ2726
-car[22]=沪FQ1230
-car[23]=沪FR1003
-car[24]=沪FR0883
-car[25]=沪FR9967
-car[26]=沪FL2989
-car[27]=沪FQ3956
-car[28]=沪FQ6687
-car[29]=沪FR7082
-car[2]=沪FQ9359
-car[30]=沪EQ3803
-car[3]=沪FR0809
-car[4]=沪FQ7108
-car[5]=沪FR3259
-car[6]=沪FQ9950
-car[7]=沪FQ7692
-car[8]=沪FR9873
-car[9]=沪FR1172

+ 0 - 335
scripts/deploy.sh

@@ -1,335 +0,0 @@
-#!/bin/bash
-###############################################################################
-# PlateRecApp 一键部署启动脚本 (fix24 v41)
-#
-# 功能:
-#   deploy   编译 + 部署 + 重启服务(默认动作)
-#   build    仅编译
-#   restart  重启 systemd 服务
-#   start    启动服务
-#   stop     停止服务
-#   status   查看服务状态 + 端口 + 进程 + 磁盘
-#   logs     跟踪运行日志 (PlateRecApp.log)
-#   journal  跟踪 systemd journal
-#   backup   手动备份当前部署
-#   rollback 回滚到上一次备份
-#   deps     检查/安装编译与运行依赖
-#   sudoers  配置 frpc 管理所需 sudoers 权限
-#
-# 用法:
-#   bash deploy.sh                 # 默认 = deploy(编译+部署+重启)
-#   bash deploy.sh build
-#   bash deploy.sh restart
-#   bash deploy.sh logs
-#   sudo bash deploy.sh deps       # 安装系统依赖需要 root
-#
-# 可用环境变量覆盖默认值:
-#   APP_DIR   部署目录   (默认 /opt/openAI/project/003.PlateRecAPP)
-#   RUN_USER  运行用户   (默认 stevenroc)
-#   SVC_NAME  systemd 服务名 (默认 PlateRecApp)
-###############################################################################
-set -euo pipefail
-
-# ============================ 配置区(可环境变量覆盖) ============================
-APP_DIR="${APP_DIR:-/opt/openAI/project/003.PlateRecAPP}"
-RUN_USER="${RUN_USER:-stevenroc}"
-SVC_NAME="${SVC_NAME:-PlateRecApp}"
-SERVICE_FILE="/etc/systemd/system/${SVC_NAME}.service"
-
-# 源码目录 = 本脚本所在目录的上一级(脚本位于 <src>/scripts/deploy.sh)
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-SRC_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
-BUILD_DIR="${SRC_DIR}/build"
-
-BIN_NAME="PlateRecApp"
-LOG_FILE="${APP_DIR}/PlateRecApp.log"
-BACKUP_ROOT="${APP_DIR}/backup"
-HEALTH_PORT="8080"          # HTTPS 服务端口(与 config.ini web_server_url 一致)
-HTTP_REDIRECT_PORT="8079"   # HTTP→HTTPS 重定向端口
-
-# 颜色输出
-RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
-BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
-info()  { echo -e "${GREEN}[✓]${NC} $*"; }
-warn()  { echo -e "${YELLOW}[!]${NC} $*"; }
-err()   { echo -e "${RED}[✗]${NC} $*" >&2; }
-step()  { echo -e "${CYAN}▶ $*${NC}"; }
-die()   { err "$*"; exit 1; }
-
-# root 检测
-is_root() { [ "$(id -u)" -eq 0 ]; }
-# 以 RUN_USER 身份执行命令(root 时降权,非 root 且用户匹配则直接执行)
-as_run_user() {
-    if is_root; then
-        if id "$RUN_USER" &>/dev/null; then
-            sudo -u "$RUN_USER" "$@"
-        else
-            warn "用户 $RUN_USER 不存在,以当前用户执行: $*"
-            "$@"
-        fi
-    else
-        "$@"
-    fi
-}
-
-# ============================ 依赖检查 ============================
-deps_check() {
-    step "检查编译/运行依赖..."
-    local missing=()
-    # 编译工具
-    for cmd in cmake make g++ pkg-config; do
-        command -v "$cmd" &>/dev/null || missing+=("$cmd")
-    done
-    # 运行工具
-    for cmd in curl ss; do
-        command -v "$cmd" &>/dev/null || missing+=("$cmd")
-    done
-    # 开发库(pkg-config / 头文件探测)
-    pkg-config --exists opencv4    2>/dev/null || missing+=("libopencv-dev")
-    pkg-config --exists openssl    2>/dev/null || missing+=("libssl-dev")
-    pkg-config --exists sqlite3    2>/dev/null || missing+=("libsqlite3-dev")
-    pkg-config --exists freetype2  2>/dev/null || missing+=("libfreetype-dev")
-    pkg-config --exists libcurl    2>/dev/null || missing+=("libcurl4-openssl-dev")
-    [ -f /usr/include/cjson/cJSON.h ] || [ -f /usr/local/include/cjson/cJSON.h ] || missing+=("libcjson-dev")
-    # HyperLPR3 SDK(源码树内)
-    [ -d "${SRC_DIR}/hyperlpr3/lib" ] || warn "未找到 ${SRC_DIR}/hyperlpr3/lib —— 编译需要 HyperLPR3 SDK,请确认 SDK 已部署到源码目录"
-
-    if [ ${#missing[@]} -gt 0 ]; then
-        warn "缺少依赖: ${missing[*]}"
-        echo "  安装命令(Debian/Ubuntu/RPi OS):"
-        echo "  sudo apt update && sudo apt install -y cmake build-essential pkg-config \\"
-        echo "    libopencv-dev libssl-dev libsqlite3-dev libfreetype-dev \\"
-        echo "    libcurl4-openssl-dev libcjson-dev curl iproute2"
-        return 1
-    fi
-    info "依赖检查通过"
-    return 0
-}
-
-deps_install() {
-    is_root || die "安装依赖需要 root,请用: sudo bash $0 deps"
-    step "安装编译与运行依赖..."
-    apt update
-    apt install -y cmake build-essential pkg-config \
-        libopencv-dev libssl-dev libsqlite3-dev libfreetype-dev \
-        libcurl4-openssl-dev libcjson-dev curl iproute2
-    info "依赖安装完成"
-    # nginx stream 模块(v2 架构需要,不强制)
-    if ! nginx -V 2>&1 | grep -q stream; then
-        warn "nginx stream 模块未安装(v2 公网架构需要): sudo apt install -y libnginx-mod-stream"
-    fi
-}
-
-# ============================ 编译 ============================
-do_build() {
-    step "编译 PlateRecApp (源码: ${SRC_DIR})"
-    deps_check || die "依赖缺失,请先运行: sudo bash $0 deps"
-    mkdir -p "${BUILD_DIR}"
-    cd "${BUILD_DIR}"
-    cmake -DCMAKE_BUILD_TYPE=Release "${SRC_DIR}"
-    make -j"$(nproc)"
-    [ -x "${BUILD_DIR}/${BIN_NAME}" ] || die "编译失败:未生成 ${BUILD_DIR}/${BIN_NAME}"
-    info "编译成功: ${BUILD_DIR}/${BIN_NAME} ($(du -h "${BUILD_DIR}/${BIN_NAME}" | cut -f1))"
-}
-
-# ============================ 部署 ============================
-ensure_dirs() {
-    step "创建部署目录结构: ${APP_DIR}"
-    mkdir -p "${APP_DIR}"/{assets/web,hyperlpr3,PlateJPG,data,scripts,ssl,backup}
-}
-
-backup_current() {
-    if [ ! -f "${APP_DIR}/${BIN_NAME}" ]; then
-        info "首次部署(${APP_DIR}/${BIN_NAME} 不存在),跳过备份"
-        return 0
-    fi
-    local ts; ts="$(date +%Y%m%d_%H%M%S)"
-    local bk="${BACKUP_ROOT}/${ts}"
-    step "备份当前版本 -> ${bk}"
-    mkdir -p "${bk}"
-    cp -a "${APP_DIR}/${BIN_NAME}" "${bk}/" 2>/dev/null || true
-    cp -a "${APP_DIR}/config.ini" "${bk}/" 2>/dev/null || true
-    cp -a "${APP_DIR}/config.txt" "${bk}/" 2>/dev/null || true
-    [ -d "${APP_DIR}/assets" ] && cp -a "${APP_DIR}/assets" "${bk}/" 2>/dev/null || true
-    [ -d "${APP_DIR}/hyperlpr3" ] && cp -a "${APP_DIR}/hyperlpr3" "${bk}/" 2>/dev/null || true
-    # 记录最新备份路径
-    echo "${bk}" > "${BACKUP_ROOT}/.latest"
-    # 只保留最近 5 个备份
-    ls -1dt "${BACKUP_ROOT}"/20* 2>/dev/null | tail -n +6 | xargs -r rm -rf
-    info "备份完成(保留最近 5 个版本)"
-}
-
-do_deploy() {
-    do_build
-    is_root || die "部署需要 root(写入 ${APP_DIR} / 安装 systemd 服务),请用: sudo bash $0 deploy"
-
-    ensure_dirs
-    backup_current
-
-    step "复制程序文件..."
-    install -m 0755 "${BUILD_DIR}/${BIN_NAME}" "${APP_DIR}/${BIN_NAME}"
-
-    # Web 前端资源(全量同步,保证 help.html 等更新生效)
-    if [ -d "${SRC_DIR}/assets/web" ]; then
-        cp -a "${SRC_DIR}/assets/web/." "${APP_DIR}/assets/web/"
-        info "Web 资源已更新 ($(ls -1 "${SRC_DIR}/assets/web" | wc -l) 个文件)"
-    fi
-
-    # HyperLPR3 SDK(模型 + 动态库),缺失时给出警告但不阻断
-    if [ -d "${SRC_DIR}/hyperlpr3" ]; then
-        cp -an "${SRC_DIR}/hyperlpr3/." "${APP_DIR}/hyperlpr3/" 2>/dev/null || true
-        info "HyperLPR3 SDK 已同步(-n 不覆盖已有模型文件)"
-    else
-        warn "源码树无 hyperlpr3/ 目录,沿用部署目录现有 SDK"
-    fi
-
-    # 运维脚本
-    cp -a "${SCRIPT_DIR}/." "${APP_DIR}/scripts/" 2>/dev/null || true
-
-    # config.ini:首次部署从模板生成;已存在则永不覆盖(保留现场配置)
-    if [ ! -f "${APP_DIR}/config.ini" ]; then
-        if [ -f "${SRC_DIR}/config.ini" ]; then
-            cp -a "${SRC_DIR}/config.ini" "${APP_DIR}/config.ini"
-            warn "已生成初始 config.ini,请按现场环境修改(RTSP/上报地址/飞书等)后重启"
-        else
-            warn "源码包中无 config.ini 模板,请手动放置到 ${APP_DIR}/config.ini"
-        fi
-    else
-        info "config.ini 已存在,保留现场配置不覆盖"
-    fi
-
-    # 动态库搜索路径:hyperlpr3/lib 加入 rpath 链接缓存(部署目录内)
-    if [ -d "${APP_DIR}/hyperlpr3/lib" ]; then
-        echo "${APP_DIR}/hyperlpr3/lib" > /etc/ld.so.conf.d/platerecapp.conf
-        ldconfig
-        info "已注册动态库路径: ${APP_DIR}/hyperlpr3/lib"
-    fi
-
-    # 目录属主
-    if id "$RUN_USER" &>/dev/null; then
-        chown -R "${RUN_USER}:${RUN_USER}" "${APP_DIR}"
-        info "目录属主已设置为 ${RUN_USER}"
-    else
-        warn "用户 ${RUN_USER} 不存在,请先创建: sudo useradd -r -s /bin/bash ${RUN_USER}"
-    fi
-
-    install_service
-    do_restart
-    sleep 3
-    do_status
-    info "部署完成 ✅  访问: https://<设备IP>:${HEALTH_PORT}/"
-}
-
-# ============================ systemd 服务 ============================
-install_service() {
-    step "安装 systemd 服务: ${SERVICE_FILE}"
-    cat > "${SERVICE_FILE}" <<EOF
-[Unit]
-Description=PlateRecApp License Plate Recognition System
-After=network-online.target frpc.service
-Wants=network-online.target
-
-[Service]
-Type=simple
-User=${RUN_USER}
-Group=${RUN_USER}
-WorkingDirectory=${APP_DIR}
-ExecStartPre=/bin/sleep 10
-ExecStart=${APP_DIR}/${BIN_NAME}
-TimeoutStartSec=60
-TimeoutStopSec=15
-Restart=on-failure
-RestartSec=5
-StandardOutput=append:${LOG_FILE}
-StandardError=append:${LOG_FILE}
-LimitNOFILE=1048576
-
-[Install]
-WantedBy=multi-user.target
-EOF
-    systemctl daemon-reload
-    systemctl enable "${SVC_NAME}" &>/dev/null || true
-    info "服务已安装并设置开机自启"
-}
-
-do_start()   { step "启动 ${SVC_NAME}"; systemctl start "${SVC_NAME}";  sleep 2; do_status; }
-do_stop()    { step "停止 ${SVC_NAME}"; systemctl stop "${SVC_NAME}";   info "已停止"; }
-do_restart() { step "重启 ${SVC_NAME}"; systemctl restart "${SVC_NAME}"; sleep 2; info "已重启"; }
-
-do_status() {
-    step "服务状态"
-    systemctl status "${SVC_NAME}" --no-pager -l 2>/dev/null || warn "服务未运行或未安装"
-    echo
-    step "端口监听"
-    ss -tlnp 2>/dev/null | grep -E ":${HEALTH_PORT}|:${HTTP_REDIRECT_PORT}" || warn "未检测到 ${HEALTH_PORT}/${HTTP_REDIRECT_PORT} 端口监听(服务可能仍在初始化,启动有 10s sleep)"
-    echo
-    step "进程"
-    pgrep -af "${BIN_NAME}" | grep -v grep || warn "无 ${BIN_NAME} 进程"
-    echo
-    step "磁盘占用 (${APP_DIR})"
-    if [ -d "${APP_DIR}" ]; then
-        du -sh "${APP_DIR}/PlateJPG" 2>/dev/null || true
-        du -sh "${APP_DIR}" 2>/dev/null || true
-    fi
-}
-
-do_logs()    { step "跟踪日志: ${LOG_FILE} (Ctrl+C 退出)"; touch "${LOG_FILE}" 2>/dev/null; tail -n 100 -f "${LOG_FILE}"; }
-do_journal() { step "journalctl -u ${SVC_NAME} (Ctrl+C 退出)"; journalctl -u "${SVC_NAME}" -f --no-pager; }
-
-# ============================ 备份/回滚 ============================
-do_backup() {
-    is_root || die "备份需要 root"
-    ensure_dirs
-    backup_current
-}
-
-do_rollback() {
-    is_root || die "回滚需要 root"
-    local bk
-    bk="$(cat "${BACKUP_ROOT}/.latest" 2>/dev/null || true)"
-    [ -n "$bk" ] && [ -d "$bk" ] || die "没有可用备份(${BACKUP_ROOT})"
-    step "回滚到: ${bk}"
-    systemctl stop "${SVC_NAME}" 2>/dev/null || true
-    [ -f "${bk}/${BIN_NAME}" ]  && cp -a "${bk}/${BIN_NAME}"  "${APP_DIR}/${BIN_NAME}"
-    [ -f "${bk}/config.ini" ]   && cp -a "${bk}/config.ini"   "${APP_DIR}/config.ini"
-    [ -f "${bk}/config.txt" ]   && cp -a "${bk}/config.txt"   "${APP_DIR}/config.txt"
-    [ -d "${bk}/assets" ]       && rm -rf "${APP_DIR}/assets" && cp -a "${bk}/assets" "${APP_DIR}/assets"
-    [ -d "${bk}/hyperlpr3" ]    && rm -rf "${APP_DIR}/hyperlpr3" && cp -a "${bk}/hyperlpr3" "${APP_DIR}/hyperlpr3"
-    chown -R "${RUN_USER}:${RUN_USER}" "${APP_DIR}" 2>/dev/null || true
-    # 回滚后移除 .latest 指向,防止重复回滚同版本
-    rm -f "${BACKUP_ROOT}/.latest"
-    systemctl start "${SVC_NAME}"
-    sleep 3
-    do_status
-    info "回滚完成"
-}
-
-# ============================ sudoers ============================
-do_sudoers() {
-    is_root || die "配置 sudoers 需要 root: sudo bash $0 sudoers"
-    step "配置 frpc 管理 sudoers 权限"
-    bash "${SCRIPT_DIR}/setup_frpc_sudoers.sh" "${RUN_USER}"
-}
-
-# ============================ 入口 ============================
-usage() {
-    sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
-    exit 0
-}
-
-case "${1:-deploy}" in
-    deploy)   do_deploy ;;
-    build)    do_build ;;
-    start)    do_start ;;
-    stop)     do_stop ;;
-    restart)  do_restart; do_status ;;
-    status)   do_status ;;
-    logs)     do_logs ;;
-    journal)  do_journal ;;
-    backup)   do_backup ;;
-    rollback) do_rollback ;;
-    deps)     deps_install ;;
-    sudoers)  do_sudoers ;;
-    -h|--help|help) usage ;;
-    *) die "未知命令: $1(运行 bash $0 help 查看用法)" ;;
-esac

+ 0 - 214
scripts/reset_passwords.py

@@ -1,214 +0,0 @@
-#!/usr/bin/env python3
-"""
-车牌识别系统 - 用户密码重置/创建脚本
-fix24 v29 (2026-07-05)
-
-用法:
-  cd /opt/openAI/project/003.PlateRecAPP
-  python3 reset_passwords.py
-
-功能:
-  - 使用 data/auth.db 认证数据库
-  - 创建或更新两个用户账号
-  - 普通用户 zhonjin (role=0)
-  - 超级管理员 admin (role=2)
-  - 同时修复 config.ini 中的 auth db_path 指向正确路径
-"""
-
-import sqlite3
-import sys
-import os
-import configparser
-
-try:
-    import bcrypt
-except ImportError:
-    print("正在安装 bcrypt 库...")
-    os.system(f"{sys.executable} -m pip install bcrypt -q")
-    import bcrypt
-
-# ==================== 配置 ====================
-# 候选数据库路径(按优先级排列)
-DB_CANDIDATES = [
-    "data/auth.db",             # fix24-v29: 唯一正确的认证数据库路径
-]
-
-CONFIG_FILE = "config.ini"
-
-USERS = [
-    {
-        "username": "zhonjin",
-        "password": "zhonjin",
-        "role": 0,        # 0=普通用户
-        "display": "普通用户"
-    },
-    {
-        "username": "admin",
-        "password": "zhonjin188A",
-        "role": 2,        # 2=超级管理员
-        "display": "超级管理员"
-    },
-]
-
-# Bcrypt cost factor (与 config.ini 中 bcrypt_cost 一致)
-BCRYPT_COST = 12
-
-# ==================== 工具函数 ====================
-def hash_password(password: str) -> str:
-    """使用 bcrypt 生成密码哈希"""
-    salt = bcrypt.gensalt(rounds=BCRYPT_COST)
-    return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
-
-def find_auth_db() -> str:
-    """自动检测正确的认证数据库"""
-    for db_path in DB_CANDIDATES:
-        if os.path.exists(db_path):
-            try:
-                conn = sqlite3.connect(db_path)
-                cursor = conn.cursor()
-                cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
-                if cursor.fetchone():
-                    conn.close()
-                    return db_path
-                conn.close()
-            except Exception:
-                continue
-    # 都不存在,返回第一个候选(让程序创建)
-    return DB_CANDIDATES[0]
-
-def fix_config_ini(correct_db_path: str):
-    """修复 config.ini 中的 auth db_path"""
-    if not os.path.exists(CONFIG_FILE):
-        print(f"⚠️  配置文件不存在: {CONFIG_FILE},跳过配置修复")
-        return
-
-    with open(CONFIG_FILE, 'r') as f:
-        content = f.read()
-
-    # 检查 [auth] 段中的 db_path
-    lines = content.split('\n')
-    new_lines = []
-    in_auth_section = False
-    db_path_found = False
-
-    for line in lines:
-        stripped = line.strip()
-        if stripped.lower() == '[auth]':
-            in_auth_section = True
-            new_lines.append(line)
-            continue
-
-        if in_auth_section and stripped.startswith('['):
-            in_auth_section = False
-
-        if in_auth_section and stripped.lower().startswith('db_path'):
-            db_path_found = True
-            new_lines.append(f'db_path={correct_db_path}')
-            print(f"🔧 config.ini: db_path 已更新为 {correct_db_path}")
-        else:
-            new_lines.append(line)
-
-    if not db_path_found:
-        # 如果没找到 db_path,在 [auth] 段的 enabled 后面添加
-        final_lines = []
-        for line in new_lines:
-            final_lines.append(line)
-            if line.strip().lower().startswith('enabled') and in_auth_section:
-                # 这里有个问题:in_auth_section 可能已经被重置了
-                pass
-        # 简单处理:直接在文件末尾的 [auth] 段添加
-        print(f"⚠️  config.ini [auth] 段未找到 db_path,请手动添加: db_path={correct_db_path}")
-    else:
-        with open(CONFIG_FILE, 'w') as f:
-            f.write('\n'.join(new_lines))
-
-# ==================== 主逻辑 ====================
-def main():
-    # 自动检测正确的数据库
-    db_path = find_auth_db()
-    print(f"📂 认证数据库: {os.path.abspath(db_path)}")
-    print(f"🔐 Bcrypt cost: {BCRYPT_COST}")
-    print()
-
-    # 确保 data/ 目录存在(如果需要创建新数据库)
-    db_dir = os.path.dirname(db_path)
-    if db_dir and not os.path.exists(db_dir):
-        os.makedirs(db_dir, exist_ok=True)
-
-    conn = sqlite3.connect(db_path)
-    cursor = conn.cursor()
-
-    # 确保 users 表存在
-    cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
-    if not cursor.fetchone():
-        print("📋 创建 users 表...")
-        cursor.execute("""
-            CREATE TABLE 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 TEXT DEFAULT CURRENT_TIMESTAMP,
-                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
-            )
-        """)
-        conn.commit()
-
-    for user in USERS:
-        username = user["username"]
-        password = user["password"]
-        role = user["role"]
-        display = user["display"]
-
-        # 生成密码哈希
-        pw_hash = hash_password(password)
-
-        # 检查用户是否已存在
-        cursor.execute("SELECT id, role FROM users WHERE username = ?", (username,))
-        existing = cursor.fetchone()
-
-        if existing:
-            # 更新密码和角色
-            cursor.execute(
-                "UPDATE users SET password_hash = ?, role = ?, enabled = 1, "
-                "failed_attempts = 0, locked_until = 0, updated_at = CURRENT_TIMESTAMP "
-                "WHERE username = ?",
-                (pw_hash, role, username)
-            )
-            print(f"✅ [{display}] {username} — 密码已更新 (role={role})")
-        else:
-            # 创建新用户
-            cursor.execute(
-                "INSERT INTO users (username, password_hash, role, enabled, "
-                "failed_attempts, locked_until, created_at, updated_at) "
-                "VALUES (?, ?, ?, 1, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
-                (username, pw_hash, role)
-            )
-            print(f"✅ [{display}] {username} — 用户已创建 (role={role})")
-
-    conn.commit()
-    conn.close()
-
-    # 修复 config.ini
-    print()
-    fix_config_ini(db_path)
-
-    print()
-    print("=" * 50)
-    print("🎉 密码重置完成!")
-    print()
-    print("账号信息:")
-    print(f"  普通用户:   zhonjin / zhonjin")
-    print(f"  超级管理员: admin / zhonjin188A")
-    print()
-    print("📌 认证数据库: " + os.path.abspath(db_path))
-    print()
-    print("⚠️  请确认 config.ini [auth] 段:")
-    print(f"     db_path={db_path}")
-    print("=" * 50)
-
-if __name__ == "__main__":
-    main()

+ 0 - 30
scripts/setup_frpc_sudoers.sh

@@ -1,30 +0,0 @@
-#!/bin/bash
-# 配置 sudoers 允许 PlateRecApp 无密码重启 frpc 服务
-# 用法: sudo bash setup_frpc_sudoers.sh [运行用户名]
-# 如果不指定用户名,自动检测当前用户
-
-RUN_USER="${1:-$(whoami)}"
-SUDOERS_FILE="/etc/sudoers.d/platerec-frpc"
-
-echo "配置 frpc 服务管理权限 for user: $RUN_USER"
-
-cat > "$SUDOERS_FILE" << EOF
-# PlateRecApp Web 管理 frpc 服务所需权限
-# 允许无密码执行 systemctl 操作 frpc 服务
-$RUN_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart frpc
-$RUN_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active frpc
-$RUN_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl status frpc
-$RUN_USER ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload frpc
-EOF
-
-chmod 440 "$SUDOERS_FILE"
-
-# 验证 sudoers 语法
-if visudo -cf "$SUDOERS_FILE"; then
-    echo "✅ sudoers 配置成功: $SUDOERS_FILE"
-    echo "用户 $RUN_USER 现在可以无密码重启 frpc 服务"
-else
-    echo "❌ sudoers 语法错误,已删除"
-    rm -f "$SUDOERS_FILE"
-    exit 1
-fi

+ 0 - 1
src/auth_middleware.cpp

@@ -26,7 +26,6 @@ const std::set<std::string>& AuthMiddleware::public_routes() {
         // Video/photo streams (keep public for external monitoring)
         "/video", "/weight", "/photo",
         "/api/weight/records",
-        "/api/sync/stats", "/api/sync/events",
         // ROI endpoints (read)
         "/api/roi/snapshot", "/api/roi/config",
         // Streaming

+ 0 - 14
src/common.cpp

@@ -56,7 +56,6 @@ int g_log_retention_days = 30;             // 默认30天
 
 // fix18: 称重系统默认配置(key与config.ini [weight]节点匹配)
 bool g_weight_enabled = false;
-bool g_flag_weight = false;          // fix27: 飞书消息是否包含重量信息(默认关闭)
 double g_weight_threshold_in = 500.0;
 double g_weight_threshold_out = 500.0;
 double g_weight_default_entry = 500.0;
@@ -85,18 +84,6 @@ std::string g_mqtt_client_id = "PlateRecApp";
 std::string g_plate_color = "蓝色";
 std::string g_vehicle_type = "货车";
 
-// ==================== fix24 v43: [sync] 并行同步配置(默认值) ====================
-bool g_sync_parallel_enabled = false;           // 默认关闭,需手动启用
-int  g_sync_reporter_timeout_sec = 30;
-int  g_sync_weight_retry_max = 5;
-int  g_sync_weight_retry_interval_sec = 10;
-int  g_sync_tb_num_dedup_sec = 300;
-int  g_sync_weight_snapshot_max_age_sec = 30;  // Bug#18: 快照优先,默认接受30秒内稳定样本
-
-// ==================== fix24 v44 P0: 预检测锁定配置(默认值) ====================
-double g_weight_prelock_threshold_kg = 9999.0;  // 预检测触发阈值(kg),默认9999,设0禁用
-int    g_weight_prelock_stable_samples = 3;      // 连续3个稳定样本(3×200ms=0.6秒)才锁定,必须≤g_weight_stable_samples(默认3)
-
 // 交替锁定
 std::map<std::string, PlateStationState> g_plate_station_cache;
 std::mutex g_station_cache_mtx;
@@ -111,7 +98,6 @@ std::mutex g_in_plate_last_processed_mtx;
 std::mutex g_out_plate_last_processed_mtx;
 
 // 飞书
-bool g_feishu_enabled = false;   // fix27: 飞书消息独立开关
 std::string g_feishu_app_id;
 std::string g_feishu_app_secret;
 std::string g_feishu_chat_id;

+ 4 - 21
src/common.h

@@ -537,10 +537,10 @@ class RTSPCapture;
 // ==================== PlateRecApp 结构体 ====================
 typedef struct {
     P_HLPR_Context hlpr_ctx;
-    RTSPCapture* capture_front_in = nullptr;
-    RTSPCapture* capture_front_out = nullptr;
-    RTSPCapture* capture_side_in = nullptr;
-    RTSPCapture* capture_side_out = nullptr;
+    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;
@@ -597,7 +597,6 @@ extern std::string g_config_ini_path;
 
 // fix18: 称重系统全局变量(config.ini key映射已在config.cpp中匹配)
 extern bool g_weight_enabled;
-extern bool g_flag_weight;           // fix27: 飞书消息是否包含重量信息(config.ini: flagWeight=1)
 extern double g_weight_threshold_in;
 extern double g_weight_threshold_out;
 extern double g_weight_default_entry;
@@ -626,21 +625,6 @@ extern std::string g_mqtt_client_id;
 extern std::string g_plate_color;
 extern std::string g_vehicle_type;
 
-// ==================== fix24 v43: [sync] 并行同步配置 ====================
-extern bool g_sync_parallel_enabled;        // [sync] enabled (0=退回v41串行, 1=启用v43并行)
-extern int  g_sync_reporter_timeout_sec;    // Reporter 等待两路结果超时(秒)
-extern int  g_sync_weight_retry_max;        // 重量独立重试最大次数
-extern int  g_sync_weight_retry_interval_sec; // 重量重试间隔(秒)
-extern int  g_sync_tb_num_dedup_sec;        // tb_num 去重窗口(秒)
-extern int  g_sync_weight_snapshot_max_age_sec; // fix24 v43 Bug#18: 重量快照最大年龄(秒)
-
-// ==================== fix24 v44 P0: 预检测锁定配置 ====================
-// 地磅场景:车辆整体开上秤台,重量一次性加载,不存在半上秤误判
-// TCP线程每200ms采样,连续 prelock_stable_samples 个样本稳定且 > prelock_threshold_kg 即锁定
-// 车牌检测后优先消费预锁定值(0等待),未命中再走快照→阻塞等待
-extern double g_weight_prelock_threshold_kg;   // 预检测触发阈值(kg),0=禁用
-extern int    g_weight_prelock_stable_samples;  // 预检测锁定时需要的连续稳定样本数
-
 // 交替锁定
 extern std::map<std::string, PlateStationState> g_plate_station_cache;
 extern std::mutex g_station_cache_mtx;
@@ -656,7 +640,6 @@ extern std::mutex g_in_plate_last_processed_mtx;
 extern std::mutex g_out_plate_last_processed_mtx;
 
 // 飞书
-extern bool g_feishu_enabled;        // fix27: 飞书消息独立开关(config.ini: [feishu] ENABLED=1)
 extern std::string g_feishu_app_id;
 extern std::string g_feishu_app_secret;
 extern std::string g_feishu_chat_id;

+ 4 - 58
src/config.cpp

@@ -90,8 +90,7 @@ static int config_ini_handler(void* user, const char* section, const char* name,
         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, "ENABLED") == 0) { g_feishu_enabled = (atoi(value) != 0); }
-        else if (strcmp(name, "APP_ID") == 0) g_feishu_app_id = value;
+        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);
@@ -99,10 +98,7 @@ static int config_ini_handler(void* user, const char* section, const char* name,
     }
     // ✅ fix18新增:称重系统配置(key与config.ini完全匹配)
     else if (section_cmp(section, "weight") == 0) {
-        if (strcmp(name, "flagWeight") == 0) {
-            g_weight_enabled = (atoi(value) == 1);
-            g_flag_weight = g_weight_enabled;  // fix27: 同步到飞书消息重量开关
-        }
+        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);
@@ -117,31 +113,10 @@ static int config_ini_handler(void* user, const char* section, const char* name,
         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);
-        // fix24 v44 P0: 预检测锁定配置
-        else if (strcmp(name, "PRELOCK_THRESHOLD_KG") == 0) {
-            g_weight_prelock_threshold_kg = atof(value);
-            if (g_weight_prelock_threshold_kg < 0) {
-                g_weight_prelock_threshold_kg = 0;  // 负值 → 禁用
-            }
-            if (g_weight_prelock_threshold_kg > 0 && g_weight_prelock_threshold_kg < 500) {
-                g_weight_prelock_threshold_kg = 500;
-            }
-            if (g_weight_prelock_threshold_kg > 60000) {
-                g_weight_prelock_threshold_kg = 60000;
-            }
-            std::cout << "[配置] 预检测阈值: " << g_weight_prelock_threshold_kg << "公斤"
-                      << (g_weight_prelock_threshold_kg == 0 ? " (已禁用)" : "") << std::endl;
-        }
-        else if (strcmp(name, "PRELOCK_STABLE_SAMPLES") == 0) {
-            g_weight_prelock_stable_samples = atoi(value);
-            if (g_weight_prelock_stable_samples < 2) g_weight_prelock_stable_samples = 2;
-            if (g_weight_prelock_stable_samples > 20) g_weight_prelock_stable_samples = 20;
-        }
     }
     // ✅ fix18新增:MQTT配置(key与config.ini完全匹配)
     else if (section_cmp(section, "mqtt") == 0) {
-        if (strcmp(name, "MQTT_ENABLED") == 0) { g_mqtt_enabled = (atoi(value) != 0); }
-        else if (strcmp(name, "MQTT_HOST") == 0) { g_mqtt_host = value; }
+        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;
@@ -165,35 +140,6 @@ static int config_ini_handler(void* user, const char* section, const char* name,
         else if (strcmp(name, "cert_path") == 0) { g_ssl_cert_path = value; }
         else if (strcmp(name, "key_path") == 0) { g_ssl_key_path = value; }
     }
-    // ==================== fix24 v43: 并行同步 [sync] ====================
-    else if (section_cmp(section, "sync") == 0) {
-        if (strcmp(name, "enabled") == 0) {
-            g_sync_parallel_enabled = (atoi(value) == 1);
-            std::cout << "[配置] [sync] enabled=" << (g_sync_parallel_enabled ? "1(并行)" : "0(串行v41)") << std::endl;
-        }
-        else if (strcmp(name, "reporter_timeout_sec") == 0) {
-            g_sync_reporter_timeout_sec = atoi(value);
-            if (g_sync_reporter_timeout_sec < 5) g_sync_reporter_timeout_sec = 5;
-        }
-        else if (strcmp(name, "weight_retry_max") == 0) {
-            g_sync_weight_retry_max = atoi(value);
-            if (g_sync_weight_retry_max < 0) g_sync_weight_retry_max = 0;
-        }
-        else if (strcmp(name, "weight_retry_interval_sec") == 0) {
-            g_sync_weight_retry_interval_sec = atoi(value);
-            if (g_sync_weight_retry_interval_sec < 1) g_sync_weight_retry_interval_sec = 1;
-        }
-        else if (strcmp(name, "tb_num_dedup_sec") == 0) {
-            g_sync_tb_num_dedup_sec = atoi(value);
-            if (g_sync_tb_num_dedup_sec < 10) g_sync_tb_num_dedup_sec = 10;
-        }
-        else if (strcmp(name, "weight_snapshot_max_age_sec") == 0) {
-            // fix24 v43 Bug#18: 重量快照最大年龄(秒),出站空车过秤快时复用近期稳定样本
-            g_sync_weight_snapshot_max_age_sec = atoi(value);
-            if (g_sync_weight_snapshot_max_age_sec < 5) g_sync_weight_snapshot_max_age_sec = 5;
-            if (g_sync_weight_snapshot_max_age_sec > 120) g_sync_weight_snapshot_max_age_sec = 120;
-        }
-    }
     // ==================== 系统监控 [system] ====================
     else if (section_cmp(section, "system") == 0) {
         if (strcmp(name, "monitor_interval") == 0) { g_sys_monitor_interval = atoi(value); }
@@ -257,7 +203,7 @@ void parse_config_ini(const char* path)
         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 << "   飞书: " << (g_feishu_enabled ? "已启用" : "未启用") << " APP_ID: " << (g_feishu_app_id.empty() ? "未配置" : "已配置") << 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;

+ 0 - 866
src/database.cpp

@@ -4,8 +4,6 @@
 #include "database.h"
 #include "utils.h"
 #include <iostream>
-#include <thread>
-#include <chrono>
 
 // 前向声明(static函数定义在后面,但init_database()需要调用)
 static void _db_load_tw_cache_unlocked();
@@ -764,135 +762,6 @@ std::vector<DbRecord> db_get_records(int page, int page_size, const std::string&
 	return records;
 }
 
-// ==================== fix24 v41: 分类查询实现 ====================
-
-// 内部:构造 WHERE 子句(不含 WHERE 关键字本身),并返回绑定参数数量
-// 通过 vector<string> 收集绑定值
-static std::string _build_where_clause(const RecordQuery& q, std::vector<std::string>& bind_vals) {
-	std::string where;
-	auto addCond = [&](const std::string& cond) {
-		if (where.empty()) where = " WHERE " + cond;
-		else where += " AND " + cond;
-	};
-
-	if (!q.plate_number.empty()) {
-		addCond("plate_number LIKE ?");
-		bind_vals.push_back("%" + q.plate_number + "%");
-	}
-	if (!q.tb_num.empty()) {
-		addCond("tb_num LIKE ?");
-		bind_vals.push_back("%" + q.tb_num + "%");
-	}
-	if (!q.date_from.empty()) {
-		addCond("create_time >= ?");
-		bind_vals.push_back(q.date_from + " 00:00:00");
-	}
-	if (!q.date_to.empty()) {
-		addCond("create_time <= ?");
-		bind_vals.push_back(q.date_to + " 23:59:59");
-	}
-	if (q.station_type == 1 || q.station_type == 2) {
-		addCond("station_type = ?");
-		bind_vals.push_back(std::to_string(q.station_type));
-	}
-	if (q.upload_status == 1) {
-		// 成功:两路均上传成功
-		addCond("lo_upload_status = 1 AND hi_upload_status = 1");
-	} else if (q.upload_status == 2) {
-		// 失败:任一路上传失败
-		addCond("(lo_upload_status != 1 OR hi_upload_status != 1)");
-	}
-	if (q.feishu_status == 1) {
-		addCond("feishu_status = 1");
-	} else if (q.feishu_status == 2) {
-		addCond("feishu_status = 2");
-	} else if (q.feishu_status == 3) {
-		addCond("feishu_status = 0");
-	}
-	return where;
-}
-
-int db_count_records_advanced(const RecordQuery& q) {
-	std::lock_guard<std::mutex> lock(g_db_mtx);
-	if (!g_db) return 0;
-
-	std::vector<std::string> bind_vals;
-	std::string where = _build_where_clause(q, bind_vals);
-	std::string sql = "SELECT COUNT(*) FROM upload_records" + where + ";";
-
-	sqlite3_stmt* stmt = nullptr;
-	if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return 0;
-
-	for (size_t i = 0; i < bind_vals.size(); ++i) {
-		sqlite3_bind_text(stmt, (int)i + 1, bind_vals[i].c_str(), -1, SQLITE_TRANSIENT);
-	}
-
-	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_advanced(int page, int page_size, const RecordQuery& q) {
-	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 = 20;
-	if (page_size > 200) page_size = 200;  // 防止过大查询
-
-	std::vector<std::string> bind_vals;
-	std::string where = _build_where_clause(q, bind_vals);
-
-	// 排序表达式与旧版保持一致
-	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 =
-		"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 + " ORDER BY " + sort_expr + " DESC LIMIT ? OFFSET ?;";
-
-	sqlite3_stmt* stmt = nullptr;
-	if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return records;
-
-	int param_idx = 1;
-	for (auto& v : bind_vals) {
-		sqlite3_bind_text(stmt, param_idx++, v.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);
-		const char* ct = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
-		rec.create_time = ct ? ct : "";
-		if (sqlite3_column_type(stmt, 2) != SQLITE_NULL)
-			rec.capture_time = sqlite3_column_int64(stmt, 2);
-		else
-			rec.capture_time = 0;
-		const char* pn = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
-		rec.plate_number = pn ? pn : "";
-		const char* tb = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
-		rec.tb_num = tb ? tb : "";
-		rec.station_type = sqlite3_column_int(stmt, 5);
-		const char* lo = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6));
-		rec.lo_photo_path = lo ? lo : "";
-		const char* hi = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 7));
-		rec.hi_photo_path = hi ? hi : "";
-		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);
@@ -1095,738 +964,3 @@ int db_get_weight_failed_count() {
     sqlite3_finalize(stmt);
     return count;
 }
-
-// fix27: 根据车牌和联单编号获取重量(用于飞书消息)
-double db_get_weight_by_plate(const std::string& plate, const std::string& tb_num) {
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return -1.0;
-    const char* sql = "SELECT weight_kg FROM weight_records WHERE plate_number = ? AND tb_num = ? ORDER BY id DESC LIMIT 1;";
-    sqlite3_stmt* stmt;
-    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) return -1.0;
-    sqlite3_bind_text(stmt, 1, plate.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_text(stmt, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
-    double weight = -1.0;
-    if (sqlite3_step(stmt) == SQLITE_ROW) {
-        weight = sqlite3_column_double(stmt, 0);
-    }
-    sqlite3_finalize(stmt);
-    return weight;
-}
-
-// fix27: 等待重量数据写入数据库(最多等待 max_wait_ms 毫秒)
-double db_wait_weight_by_plate(const std::string& plate, const std::string& tb_num, int max_wait_ms) {
-    // 每 200ms 检查一次,最多等待 max_wait_ms
-    const int check_interval_ms = 200;
-    const int max_checks = max_wait_ms / check_interval_ms;
-    
-    for (int i = 0; i < max_checks; i++) {
-        double weight = db_get_weight_by_plate(plate, tb_num);
-        if (weight >= 0) {
-            return weight;  // 找到重量数据
-        }
-        std::this_thread::sleep_for(std::chrono::milliseconds(check_interval_ms));
-    }
-    return -1.0;  // 超时未找到
-}
-
-// ==================== fix24 v41: 称重记录分类查询 ====================
-
-// 构建 WHERE 子句,返回参数化占位符;bind_values 收集需要绑定的值
-// 返回值: WHERE 子句字符串(不含 "WHERE" 关键字),空串表示无条件
-static std::string _build_weight_where_clause(const WeightQuery& q,
-    std::vector<std::string>& bind_texts, std::vector<int>& bind_ints) {
-    std::string where;
-    bind_texts.clear();
-    bind_ints.clear();
-
-    if (!q.plate_number.empty()) {
-        if (!where.empty()) where += " AND ";
-        where += "plate_number LIKE ?";
-        bind_texts.push_back("%" + q.plate_number + "%");
-    }
-    if (!q.tb_num.empty()) {
-        if (!where.empty()) where += " AND ";
-        where += "tb_num LIKE ?";
-        bind_texts.push_back("%" + q.tb_num + "%");
-    }
-    if (!q.date_from.empty()) {
-        if (!where.empty()) where += " AND ";
-        where += "create_time >= ?";
-        bind_texts.push_back(q.date_from + " 00:00:00");
-    }
-    if (!q.date_to.empty()) {
-        if (!where.empty()) where += " AND ";
-        where += "create_time <= ?";
-        bind_texts.push_back(q.date_to + " 23:59:59");
-    }
-    if (q.station_type == 1 || q.station_type == 2) {
-        if (!where.empty()) where += " AND ";
-        where += "station_type = ?";
-        bind_ints.push_back(q.station_type);
-    }
-    if (q.upload_status == 1) {
-        // 成功
-        if (!where.empty()) where += " AND ";
-        where += "upload_status = 1";
-    } else if (q.upload_status == 2) {
-        // 失败
-        if (!where.empty()) where += " AND ";
-        where += "upload_status = 2";
-    } else if (q.upload_status == 3) {
-        // 待上传
-        if (!where.empty()) where += " AND ";
-        where += "upload_status = 0";
-    }
-    return where;
-}
-
-int db_count_weight_records_advanced(const WeightQuery& q) {
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return 0;
-
-    std::vector<std::string> bind_texts;
-    std::vector<int> bind_ints;
-    std::string where = _build_weight_where_clause(q, bind_texts, bind_ints);
-
-    std::string sql = "SELECT COUNT(*) FROM weight_records";
-    if (!where.empty()) sql += " WHERE " + where;
-    sql += ";";
-
-    sqlite3_stmt* stmt;
-    if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) return 0;
-
-    int idx = 1;
-    for (auto& v : bind_texts) sqlite3_bind_text(stmt, idx++, v.c_str(), -1, SQLITE_TRANSIENT);
-    for (auto& v : bind_ints) sqlite3_bind_int(stmt, idx++, v);
-
-    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_weight_records_advanced(int page, int page_size, const WeightQuery& q) {
-    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 = 20;
-    if (page_size > 200) page_size = 200;
-    int offset = (page - 1) * page_size;
-
-    std::vector<std::string> bind_texts;
-    std::vector<int> bind_ints;
-    std::string where = _build_weight_where_clause(q, bind_texts, bind_ints);
-
-    std::string sql = "SELECT id, create_time, plate_number, tb_num, station_type, weight_kg, upload_status, retry_count, response_msg FROM weight_records";
-    if (!where.empty()) sql += " WHERE " + where;
-    sql += " 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;
-
-    int idx = 1;
-    for (auto& v : bind_texts) sqlite3_bind_text(stmt, idx++, v.c_str(), -1, SQLITE_TRANSIENT);
-    for (auto& v : bind_ints) sqlite3_bind_int(stmt, idx++, v);
-    sqlite3_bind_int(stmt, idx++, page_size);
-    sqlite3_bind_int(stmt, idx++, offset);
-
-    while (sqlite3_step(stmt) == SQLITE_ROW) {
-        DbRecord rec;
-        rec.id = sqlite3_column_int(stmt, 0);
-        const char* ct = (const char*)sqlite3_column_text(stmt, 1);
-        rec.create_time = ct ? ct : "";
-        const char* pn = (const char*)sqlite3_column_text(stmt, 2);
-        rec.plate_number = pn ? pn : "";
-        const char* tn = (const char*)sqlite3_column_text(stmt, 3);
-        rec.tb_num = tn ? tn : "";
-        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;
-}
-
-// ==================== fix24 v41: 照片清理DB同步 ====================
-
-/**
- * 从完整路径中提取文件名(basename)
- * 例: /opt/openAI/.../PlateJPG/沪FQ7108_1787365677_side_out.jpg -> 沪FQ7108_1787365677_side_out.jpg
- */
-static std::string extract_basename(const std::string& path) {
-    size_t pos = path.find_last_of("/\\");
-    if (pos == std::string::npos) return path;
-    return path.substr(pos + 1);
-}
-
-/**
- * 清空指定路径对应的照片字段(fix24 v41 修复版)
- * 当 daily_file_cleanup 物理删除照片后调用,同步清空 upload_records 表中的路径字段。
- * 保留业务记录(车牌号、联单编号等),只清空 lo_photo_path/hi_photo_path。
- *
- * 关键修复:DB 中存储的是相对文件名(如 沪FQ7108_xxx.jpg),
- * 而 daily_file_cleanup 传入的是绝对路径(如 /opt/.../PlateJPG/沪FQ7108_xxx.jpg),
- * 因此必须提取 basename 再匹配,否则永远匹配不到。
- *
- * @param paths 被删除的照片完整路径列表(绝对路径或相对文件名均可)
- * @return 成功更新的字段数(lo + hi 合计)
- */
-int db_clear_photo_paths_by_paths(const std::vector<std::string>& paths) {
-    if (paths.empty()) return 0;
-
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return -1;
-
-    int updated_count = 0;
-
-    // 用一个事务批量执行,提升性能
-    sqlite3_exec(g_db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
-
-    const char* sql_lo = "UPDATE upload_records SET lo_photo_path = NULL WHERE lo_photo_path = ?;";
-    const char* sql_hi = "UPDATE upload_records SET hi_photo_path = NULL WHERE hi_photo_path = ?;";
-
-    for (const auto& path : paths) {
-        // 提取文件名进行匹配(DB 存储的是相对文件名)
-        std::string basename = extract_basename(path);
-        if (basename.empty()) continue;
-
-        // 清空 lo_photo_path
-        sqlite3_stmt* stmt = nullptr;
-        if (sqlite3_prepare_v2(g_db, sql_lo, -1, &stmt, nullptr) == SQLITE_OK) {
-            sqlite3_bind_text(stmt, 1, basename.c_str(), -1, SQLITE_TRANSIENT);
-            if (sqlite3_step(stmt) == SQLITE_DONE) {
-                updated_count += sqlite3_changes(g_db);
-            }
-            sqlite3_finalize(stmt);
-        }
-
-        // 清空 hi_photo_path
-        stmt = nullptr;
-        if (sqlite3_prepare_v2(g_db, sql_hi, -1, &stmt, nullptr) == SQLITE_OK) {
-            sqlite3_bind_text(stmt, 1, basename.c_str(), -1, SQLITE_TRANSIENT);
-            if (sqlite3_step(stmt) == SQLITE_DONE) {
-                updated_count += sqlite3_changes(g_db);
-            }
-            sqlite3_finalize(stmt);
-        }
-    }
-
-    sqlite3_exec(g_db, "COMMIT;", nullptr, nullptr, nullptr);
-    return updated_count;
-}
-
-/**
- * 启动时清理孤儿照片路径(fix24 v41 新增)
- *
- * 扫描 upload_records 表中所有 lo_photo_path / hi_photo_path 非 NULL 的记录,
- * 检查对应文件是否还存在于 PlateJPG/ 目录下;不存在的清空路径字段。
- * 用于修复历史遗留数据(照片已被旧版本清理但 DB 路径未同步)。
- *
- * @param photo_dir PlateJPG 目录的绝对路径(以 / 结尾)
- * @return 成功清空的字段数
- */
-int db_purge_missing_photo_paths(const std::string& photo_dir) {
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return -1;
-
-    int purged = 0;
-    std::vector<std::pair<int64_t, std::string>> lo_paths, hi_paths;
-
-    // 收集所有非空路径
-    const char* select_sql =
-        "SELECT id, lo_photo_path, hi_photo_path FROM upload_records "
-        "WHERE lo_photo_path IS NOT NULL OR hi_photo_path IS NOT NULL;";
-
-    sqlite3_stmt* stmt = nullptr;
-    if (sqlite3_prepare_v2(g_db, select_sql, -1, &stmt, nullptr) != SQLITE_OK) {
-        std::cerr << "[DB清理] 查询失败: " << sqlite3_errmsg(g_db) << std::endl;
-        return -1;
-    }
-
-    while (sqlite3_step(stmt) == SQLITE_ROW) {
-        int64_t id = sqlite3_column_int64(stmt, 0);
-        const char* lo = (const char*)sqlite3_column_text(stmt, 1);
-        const char* hi = (const char*)sqlite3_column_text(stmt, 2);
-        if (lo && lo[0]) lo_paths.emplace_back(id, lo);
-        if (hi && hi[0]) hi_paths.emplace_back(id, hi);
-    }
-    sqlite3_finalize(stmt);
-
-    if (lo_paths.empty() && hi_paths.empty()) {
-        std::cout << "[DB清理] 无需清理的孤儿路径" << std::endl;
-        return 0;
-    }
-
-    std::cout << "[DB清理] 检查 " << lo_paths.size() << " 条lo路径 + "
-              << hi_paths.size() << " 条hi路径..." << std::endl;
-
-    // 检查 lo_photo_path
-    sqlite3_exec(g_db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
-
-    const char* update_lo = "UPDATE upload_records SET lo_photo_path = NULL WHERE id = ?;";
-    for (const auto& [id, filename] : lo_paths) {
-        std::string full = photo_dir + filename;
-        struct stat st;
-        if (stat(full.c_str(), &st) != 0) {
-            // 文件不存在,清空
-            sqlite3_stmt* u = nullptr;
-            if (sqlite3_prepare_v2(g_db, update_lo, -1, &u, nullptr) == SQLITE_OK) {
-                sqlite3_bind_int64(u, 1, id);
-                if (sqlite3_step(u) == SQLITE_DONE) purged++;
-                sqlite3_finalize(u);
-            }
-        }
-    }
-
-    const char* update_hi = "UPDATE upload_records SET hi_photo_path = NULL WHERE id = ?;";
-    for (const auto& [id, filename] : hi_paths) {
-        std::string full = photo_dir + filename;
-        struct stat st;
-        if (stat(full.c_str(), &st) != 0) {
-            sqlite3_stmt* u = nullptr;
-            if (sqlite3_prepare_v2(g_db, update_hi, -1, &u, nullptr) == SQLITE_OK) {
-                sqlite3_bind_int64(u, 1, id);
-                if (sqlite3_step(u) == SQLITE_DONE) purged++;
-                sqlite3_finalize(u);
-            }
-        }
-    }
-
-    sqlite3_exec(g_db, "COMMIT;", nullptr, nullptr, nullptr);
-
-    std::cout << "[DB清理] 清空 " << purged << " 个孤儿路径字段" << std::endl;
-    return purged;
-}
-// ==================== fix24 v43: 原子写入两表 ====================
-
-int db_insert_event_atomic(
-    // upload_records 字段
-    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,
-    // weight_records 字段
-    double weight_kg, int weight_upload_status, const std::string& weight_response_msg)
-{
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return -1;
-
-    // 开启事务
-    char* errmsg = nullptr;
-    int rc = sqlite3_exec(g_db, "BEGIN TRANSACTION;", nullptr, nullptr, &errmsg);
-    if (rc != SQLITE_OK) {
-        std::cerr << "[v43-DB] BEGIN 失败: " << (errmsg ? errmsg : "unknown") << std::endl;
-        if (errmsg) sqlite3_free(errmsg);
-        return -1;
-    }
-
-    // 写入 upload_records
-    const char* sql1 = R"(
-        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)
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
-    )";
-    sqlite3_stmt* stmt1 = nullptr;
-    if (sqlite3_prepare_v2(g_db, sql1, -1, &stmt1, nullptr) != SQLITE_OK) {
-        std::cerr << "[v43-DB] prepare upload_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
-        sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
-        return -1;
-    }
-    sqlite3_bind_text(stmt1, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_text(stmt1, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_int(stmt1, 3, station_type);
-    sqlite3_bind_text(stmt1, 4, lo_photo_path.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_text(stmt1, 5, hi_photo_path.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_int(stmt1, 6, lo_upload_status);
-    sqlite3_bind_int(stmt1, 7, hi_upload_status);
-    sqlite3_bind_int(stmt1, 8, feishu_status);
-    sqlite3_bind_int64(stmt1, 9, (long long)capture_time);
-
-    rc = sqlite3_step(stmt1);
-    sqlite3_finalize(stmt1);
-    if (rc != SQLITE_DONE) {
-        std::cerr << "[v43-DB] INSERT upload_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
-        sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
-        return -1;
-    }
-
-    // 写入 weight_records
-    const char* sql2 = R"(
-        INSERT INTO weight_records
-        (plate_number, tb_num, station_type, weight_kg, upload_status, response_msg)
-        VALUES (?, ?, ?, ?, ?, ?);
-    )";
-    sqlite3_stmt* stmt2 = nullptr;
-    if (sqlite3_prepare_v2(g_db, sql2, -1, &stmt2, nullptr) != SQLITE_OK) {
-        std::cerr << "[v43-DB] prepare weight_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
-        sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
-        return -1;
-    }
-    sqlite3_bind_text(stmt2, 1, plate_number.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_text(stmt2, 2, tb_num.c_str(), -1, SQLITE_TRANSIENT);
-    sqlite3_bind_int(stmt2, 3, station_type);
-    sqlite3_bind_double(stmt2, 4, weight_kg);
-    sqlite3_bind_int(stmt2, 5, weight_upload_status);
-    sqlite3_bind_text(stmt2, 6, weight_response_msg.c_str(), -1, SQLITE_TRANSIENT);
-
-    rc = sqlite3_step(stmt2);
-    sqlite3_finalize(stmt2);
-    if (rc != SQLITE_DONE) {
-        std::cerr << "[v43-DB] INSERT weight_records 失败: " << sqlite3_errmsg(g_db) << std::endl;
-        sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
-        return -1;
-    }
-
-    // 提交事务
-    rc = sqlite3_exec(g_db, "COMMIT;", nullptr, nullptr, &errmsg);
-    if (rc != SQLITE_OK) {
-        std::cerr << "[v43-DB] COMMIT 失败: " << (errmsg ? errmsg : "unknown") << std::endl;
-        if (errmsg) sqlite3_free(errmsg);
-        sqlite3_exec(g_db, "ROLLBACK;", nullptr, nullptr, nullptr);
-        return -1;
-    }
-
-    std::cout << "[v43-DB] 原子写入成功: " << plate_number << " 联单:" << tb_num
-              << " 重量:" << weight_kg << "kg" << std::endl;
-    return 0;
-}
-
-// ==================== fix24 v43: 按 tb_num 联合查询 ====================
-
-std::vector<EventRecord> db_get_event_records_by_tbnum(const std::string& tb_num) {
-    std::vector<EventRecord> results;
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return results;
-
-    const char* sql = R"(
-        SELECT
-            ur.id AS ur_id,
-            ur.plate_number,
-            ur.tb_num,
-            ur.station_type,
-            ur.create_time,
-            ur.capture_time,
-            ur.lo_photo_path,
-            ur.hi_photo_path,
-            ur.lo_upload_status,
-            ur.hi_upload_status,
-            ur.feishu_status,
-            wr.id AS wr_id,
-            wr.weight_kg,
-            wr.upload_status AS weight_upload_status,
-            wr.response_msg AS weight_response_msg
-        FROM upload_records ur
-        LEFT JOIN weight_records wr ON wr.id = (
-            SELECT MAX(w2.id) FROM weight_records w2
-            WHERE w2.tb_num = ur.tb_num AND w2.plate_number = ur.plate_number
-              AND w2.station_type = ur.station_type
-        )
-        WHERE ur.tb_num LIKE ?
-        ORDER BY ur.create_time DESC;
-    )";
-
-    sqlite3_stmt* stmt = nullptr;
-    if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
-        std::cerr << "[v43-DB] prepare 联合查询失败: " << sqlite3_errmsg(g_db) << std::endl;
-        return results;
-    }
-
-    std::string pattern = "%" + tb_num + "%";
-    sqlite3_bind_text(stmt, 1, pattern.c_str(), -1, SQLITE_TRANSIENT);
-
-    while (sqlite3_step(stmt) == SQLITE_ROW) {
-        EventRecord rec;
-        rec.ur_id = sqlite3_column_int(stmt, 0);
-        const char* s;
-        s = (const char*)sqlite3_column_text(stmt, 1); if (s) rec.plate_number = s;
-        s = (const char*)sqlite3_column_text(stmt, 2); if (s) rec.tb_num = s;
-        rec.station_type = sqlite3_column_int(stmt, 3);
-        s = (const char*)sqlite3_column_text(stmt, 4); if (s) rec.create_time = s;
-        // capture_time 存储为 Unix 时间戳 (INTEGER),需转换为格式化字符串
-        if (sqlite3_column_type(stmt, 5) != SQLITE_NULL) {
-            int64_t cap_ts = sqlite3_column_int64(stmt, 5);
-            struct tm tm_buf;
-            time_t t = (time_t)cap_ts;
-            localtime_r(&t, &tm_buf);
-            char buf[32];
-            strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
-            rec.capture_time = buf;
-        }
-        s = (const char*)sqlite3_column_text(stmt, 6); if (s) rec.lo_photo_path = s;
-        s = (const char*)sqlite3_column_text(stmt, 7); if (s) rec.hi_photo_path = s;
-        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.wr_id = sqlite3_column_int(stmt, 11);
-        rec.weight_kg = sqlite3_column_double(stmt, 12);
-        rec.weight_upload_status = sqlite3_column_int(stmt, 13);
-        s = (const char*)sqlite3_column_text(stmt, 14); if (s) rec.weight_response_msg = s;
-        results.push_back(rec);
-    }
-    sqlite3_finalize(stmt);
-    return results;
-}
-
-// ==================== fix24 v43 P2: 分页联合查询 + 同步统计 ====================
-
-std::vector<EventRecord> db_get_event_records_joined(int page, int page_size, const SyncQuery& q) {
-    std::vector<EventRecord> results;
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return results;
-
-    std::string sql = R"(
-        SELECT
-            ur.id AS ur_id,
-            ur.plate_number,
-            ur.tb_num,
-            ur.station_type,
-            ur.create_time,
-            ur.capture_time,
-            ur.lo_photo_path,
-            ur.hi_photo_path,
-            ur.lo_upload_status,
-            ur.hi_upload_status,
-            ur.feishu_status,
-            wr.id AS wr_id,
-            wr.weight_kg,
-            wr.upload_status AS weight_upload_status,
-            wr.response_msg AS weight_response_msg
-        FROM upload_records ur
-        LEFT JOIN weight_records wr ON wr.id = (
-            SELECT MAX(w2.id) FROM weight_records w2
-            WHERE w2.tb_num = ur.tb_num AND w2.plate_number = ur.plate_number
-              AND w2.station_type = ur.station_type
-        )
-        WHERE 1=1
-    )";
-
-    std::vector<std::string> binds;
-
-    if (!q.plate_number.empty()) {
-        sql += " AND ur.plate_number LIKE ?";
-        binds.push_back("%" + q.plate_number + "%");
-    }
-    if (!q.tb_num.empty()) {
-        sql += " AND ur.tb_num LIKE ?";
-        binds.push_back("%" + q.tb_num + "%");
-    }
-    if (!q.date_from.empty()) {
-        sql += " AND ur.create_time >= ?";
-        binds.push_back(q.date_from + " 00:00:00");
-    }
-    if (!q.date_to.empty()) {
-        sql += " AND ur.create_time <= ?";
-        binds.push_back(q.date_to + " 23:59:59");
-    }
-    if (q.station_type > 0) {
-        sql += " AND ur.station_type = ?";
-        binds.push_back(std::to_string(q.station_type));
-    }
-    if (q.sync_status == 1) {
-        sql += " AND wr.id IS NOT NULL";
-    } else if (q.sync_status == 2) {
-        sql += " AND wr.id IS NULL";
-    } else if (q.sync_status == 3) {
-        sql += " AND wr.id IS NOT NULL AND wr.upload_status = 2";
-    }
-
-    sql += " ORDER BY ur.create_time DESC";
-
-    // 分页
-    int offset = (page - 1) * page_size;
-    sql += " LIMIT ? OFFSET ?";
-
-    sqlite3_stmt* stmt = nullptr;
-    if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
-        std::cerr << "[v43-P2] prepare 联合查询失败: " << sqlite3_errmsg(g_db) << std::endl;
-        return results;
-    }
-
-    int idx = 1;
-    for (const auto& b : binds) {
-        sqlite3_bind_text(stmt, idx++, b.c_str(), -1, SQLITE_TRANSIENT);
-    }
-    sqlite3_bind_int(stmt, idx++, page_size);
-    sqlite3_bind_int(stmt, idx++, offset);
-
-    while (sqlite3_step(stmt) == SQLITE_ROW) {
-        EventRecord rec;
-        rec.ur_id = sqlite3_column_int(stmt, 0);
-        const char* s;
-        s = (const char*)sqlite3_column_text(stmt, 1); if (s) rec.plate_number = s;
-        s = (const char*)sqlite3_column_text(stmt, 2); if (s) rec.tb_num = s;
-        rec.station_type = sqlite3_column_int(stmt, 3);
-        s = (const char*)sqlite3_column_text(stmt, 4); if (s) rec.create_time = s;
-        // capture_time 存储为 Unix 时间戳 (INTEGER),需转换为格式化字符串
-        if (sqlite3_column_type(stmt, 5) != SQLITE_NULL) {
-            int64_t cap_ts = sqlite3_column_int64(stmt, 5);
-            struct tm tm_buf;
-            time_t t = (time_t)cap_ts;
-            localtime_r(&t, &tm_buf);
-            char buf[32];
-            strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm_buf);
-            rec.capture_time = buf;
-        }
-        s = (const char*)sqlite3_column_text(stmt, 6); if (s) rec.lo_photo_path = s;
-        s = (const char*)sqlite3_column_text(stmt, 7); if (s) rec.hi_photo_path = s;
-        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.wr_id = sqlite3_column_int(stmt, 11);
-        rec.weight_kg = sqlite3_column_double(stmt, 12);
-        rec.weight_upload_status = sqlite3_column_int(stmt, 13);
-        s = (const char*)sqlite3_column_text(stmt, 14); if (s) rec.weight_response_msg = s;
-        results.push_back(rec);
-    }
-    sqlite3_finalize(stmt);
-    return results;
-}
-
-int db_count_event_records_joined(const SyncQuery& q) {
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return 0;
-
-    std::string sql = R"(
-        SELECT COUNT(*)
-        FROM upload_records ur
-        LEFT JOIN weight_records wr ON wr.id = (
-            SELECT MAX(w2.id) FROM weight_records w2
-            WHERE w2.tb_num = ur.tb_num AND w2.plate_number = ur.plate_number
-              AND w2.station_type = ur.station_type
-        )
-        WHERE 1=1
-    )";
-
-    std::vector<std::string> binds;
-
-    if (!q.plate_number.empty()) {
-        sql += " AND ur.plate_number LIKE ?";
-        binds.push_back("%" + q.plate_number + "%");
-    }
-    if (!q.tb_num.empty()) {
-        sql += " AND ur.tb_num LIKE ?";
-        binds.push_back("%" + q.tb_num + "%");
-    }
-    if (!q.date_from.empty()) {
-        sql += " AND ur.create_time >= ?";
-        binds.push_back(q.date_from + " 00:00:00");
-    }
-    if (!q.date_to.empty()) {
-        sql += " AND ur.create_time <= ?";
-        binds.push_back(q.date_to + " 23:59:59");
-    }
-    if (q.station_type > 0) {
-        sql += " AND ur.station_type = ?";
-        binds.push_back(std::to_string(q.station_type));
-    }
-    if (q.sync_status == 1) {
-        sql += " AND wr.id IS NOT NULL";
-    } else if (q.sync_status == 2) {
-        sql += " AND wr.id IS NULL";
-    } else if (q.sync_status == 3) {
-        sql += " AND wr.id IS NOT NULL AND wr.upload_status = 2";
-    }
-
-    sqlite3_stmt* stmt = nullptr;
-    if (sqlite3_prepare_v2(g_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
-        std::cerr << "[v43-P2] prepare count 联合查询失败: " << sqlite3_errmsg(g_db) << std::endl;
-        return 0;
-    }
-
-    int idx = 1;
-    for (const auto& b : binds) {
-        sqlite3_bind_text(stmt, idx++, b.c_str(), -1, SQLITE_TRANSIENT);
-    }
-
-    int count = 0;
-    if (sqlite3_step(stmt) == SQLITE_ROW) {
-        count = sqlite3_column_int(stmt, 0);
-    }
-    sqlite3_finalize(stmt);
-    return count;
-}
-
-SyncStats db_get_sync_stats() {
-    SyncStats stats;
-    std::lock_guard<std::mutex> lock(g_db_mtx);
-    if (!g_db) return stats;
-
-    // 总上传记录数
-    const char* sql1 = "SELECT COUNT(*) FROM upload_records;";
-    sqlite3_stmt* stmt = nullptr;
-    if (sqlite3_prepare_v2(g_db, sql1, -1, &stmt, nullptr) == SQLITE_OK) {
-        if (sqlite3_step(stmt) == SQLITE_ROW) stats.total_uploads = sqlite3_column_int(stmt, 0);
-        sqlite3_finalize(stmt);
-    }
-
-    // 今日上传记录数
-    const char* sql2 = "SELECT COUNT(*) FROM upload_records WHERE date(create_time) = date('now', 'localtime');";
-    if (sqlite3_prepare_v2(g_db, sql2, -1, &stmt, nullptr) == SQLITE_OK) {
-        if (sqlite3_step(stmt) == SQLITE_ROW) stats.today_uploads = sqlite3_column_int(stmt, 0);
-        sqlite3_finalize(stmt);
-    }
-
-    // 有重量匹配的记录数(两表 tb_num + station_type 关联成功)
-    // fix24 v43 Bug#16: 必须带 station_type,同一联单进出站共用 tb_num,
-    // 缺条件会导致进站行错误匹配到出站重量(同联单最新记录)
-    const char* sql3 = R"(
-        SELECT COUNT(DISTINCT ur.id)
-        FROM upload_records ur
-        INNER JOIN weight_records wr ON ur.tb_num = wr.tb_num
-            AND ur.plate_number = wr.plate_number
-            AND ur.station_type = wr.station_type;
-    )";
-    if (sqlite3_prepare_v2(g_db, sql3, -1, &stmt, nullptr) == SQLITE_OK) {
-        if (sqlite3_step(stmt) == SQLITE_ROW) stats.matched_weight = sqlite3_column_int(stmt, 0);
-        sqlite3_finalize(stmt);
-    }
-
-    // 今日匹配数(Bug#16: 同样补 station_type)
-    const char* sql4 = R"(
-        SELECT COUNT(DISTINCT ur.id)
-        FROM upload_records ur
-        INNER JOIN weight_records wr ON ur.tb_num = wr.tb_num
-            AND ur.plate_number = wr.plate_number
-            AND ur.station_type = wr.station_type
-        WHERE date(ur.create_time) = date('now', 'localtime');
-    )";
-    if (sqlite3_prepare_v2(g_db, sql4, -1, &stmt, nullptr) == SQLITE_OK) {
-        if (sqlite3_step(stmt) == SQLITE_ROW) stats.today_matched = sqlite3_column_int(stmt, 0);
-        sqlite3_finalize(stmt);
-    }
-
-    // 重量上传状态统计
-    const char* sql5 = R"(
-        SELECT
-            SUM(CASE WHEN upload_status = 1 THEN 1 ELSE 0 END) AS ok_count,
-            SUM(CASE WHEN upload_status = 2 THEN 1 ELSE 0 END) AS fail_count,
-            SUM(CASE WHEN upload_status = 0 THEN 1 ELSE 0 END) AS pending_count
-        FROM weight_records;
-    )";
-    if (sqlite3_prepare_v2(g_db, sql5, -1, &stmt, nullptr) == SQLITE_OK) {
-        if (sqlite3_step(stmt) == SQLITE_ROW) {
-            stats.weight_upload_ok = sqlite3_column_int(stmt, 0);
-            stats.weight_upload_fail = sqlite3_column_int(stmt, 1);
-            stats.weight_pending = sqlite3_column_int(stmt, 2);
-        }
-        sqlite3_finalize(stmt);
-    }
-
-    // 计算匹配率
-    if (stats.total_uploads > 0) {
-        stats.match_rate = (double)stats.matched_weight / stats.total_uploads * 100.0;
-    }
-    if (stats.today_uploads > 0) {
-        stats.today_match_rate = (double)stats.today_matched / stats.today_uploads * 100.0;
-    }
-
-    return stats;
-}

+ 1 - 114
src/database.h

@@ -16,22 +16,6 @@ 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);
-
-// ==================== fix24 v41: 分类查询条件结构体 ====================
-struct RecordQuery {
-    std::string plate_number;   // 车牌号(模糊匹配)
-    std::string tb_num;         // 联单编号(模糊匹配)
-    std::string date_from;      // 起始日期 YYYY-MM-DD
-    std::string date_to;        // 截止日期 YYYY-MM-DD
-    int station_type = 0;       // 0=全部, 1=进站, 2=出站
-    int upload_status = 0;      // 0=全部, 1=成功, 2=失败
-    int feishu_status = 0;      // 0=全部, 1=已发送, 2=失败, 3=未发送
-};
-
-// fix24 v41: 分类查询 + 总数统计
-std::vector<DbRecord> db_get_records_advanced(int page, int page_size, const RecordQuery& q);
-int db_count_records_advanced(const RecordQuery& q);
-
 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);
@@ -49,102 +33,5 @@ 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();
-// fix27: 根据车牌和联单编号获取重量(用于飞书消息)
-double db_get_weight_by_plate(const std::string& plate, const std::string& tb_num);
-// fix27: 等待重量数据写入数据库(最多等待 max_wait_ms 毫秒)
-double db_wait_weight_by_plate(const std::string& plate, const std::string& tb_num, int max_wait_ms = 3000);
-
-// ==================== fix24 v41: 称重记录分类查询 ====================
-struct WeightQuery {
-    std::string plate_number;   // 车牌号(模糊匹配)
-    std::string tb_num;         // 联单编号(模糊匹配)
-    std::string date_from;      // 起始日期 YYYY-MM-DD
-    std::string date_to;        // 截止日期 YYYY-MM-DD
-    int station_type = 0;       // 0=全部, 1=进站, 2=出站
-    int upload_status = 0;      // 0=全部, 1=成功, 2=失败, 3=待上传
-};
-
-std::vector<DbRecord> db_get_weight_records_advanced(int page, int page_size, const WeightQuery& q);
-int db_count_weight_records_advanced(const WeightQuery& q);
-
-// ==================== fix24 v41: 照片清理DB同步 ====================
-// 清空指定路径对应的照片字段(照片被物理删除后调用,自动提取basename匹配)
-int db_clear_photo_paths_by_paths(const std::vector<std::string>& paths);
-// 启动时扫描DB,清空文件已不存在的孤儿照片路径
-int db_purge_missing_photo_paths(const std::string& photo_dir);
-
-// ==================== fix24 v43: 原子写入两表 ====================
-/**
- * 在一次 SQLite 事务内同时写入 upload_records 和 weight_records。
- * 保证 tb_num 关联的原子性:要么两表都写入成功,要么都不写入。
- *
- * @return 0=成功, -1=失败(自动 rollback)
- */
-int db_insert_event_atomic(
-    // upload_records 字段
-    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,
-    // weight_records 字段
-    double weight_kg, int weight_upload_status, const std::string& weight_response_msg);
-
-/**
- * 按 tb_num 联合查询 upload_records + weight_records。
- * 返回两表合并的结果(用于 Web 展示)。
- */
-struct EventRecord {
-    // 照片路
-    int    ur_id = 0;
-    std::string plate_number;
-    std::string tb_num;
-    int    station_type = 0;
-    std::string create_time;
-    std::string capture_time;
-    std::string lo_photo_path;
-    std::string hi_photo_path;
-    int    lo_upload_status = 0;
-    int    hi_upload_status = 0;
-    int    feishu_status = 0;
-    // 重量路
-    int    wr_id = 0;
-    double weight_kg = 0.0;
-    int    weight_upload_status = 0;
-    std::string weight_response_msg;
-};
-
-std::vector<EventRecord> db_get_event_records_by_tbnum(const std::string& tb_num);
-
-// ==================== fix24 v43 P2: 分页联合查询 + 同步统计 ====================
-
-// 联合查询条件(复用 EventRecord 作为返回类型)
-struct SyncQuery {
-    std::string plate_number;
-    std::string tb_num;
-    std::string date_from;
-    std::string date_to;
-    int station_type = 0;         // 0=全部, 1=进站, 2=出站
-    int sync_status = 0;          // 0=全部, 1=已匹配, 2=未匹配(无重量), 3=重量上传失败
-};
-
-// 分页联合查询(upload_records LEFT JOIN weight_records)
-std::vector<EventRecord> db_get_event_records_joined(int page, int page_size, const SyncQuery& q);
-int db_count_event_records_joined(const SyncQuery& q);
-
-// 同步统计摘要
-struct SyncStats {
-    int total_uploads = 0;        // upload_records 总条数
-    int today_uploads = 0;        // 今日 upload_records 条数
-    int matched_weight = 0;       // 有重量匹配的记录数
-    int today_matched = 0;        // 今日匹配数
-    int weight_upload_ok = 0;     // 重量上传成功数
-    int weight_upload_fail = 0;   // 重量上传失败数
-    int weight_pending = 0;       // 重量待上传数
-    double match_rate = 0.0;      // 匹配率 %
-    double today_match_rate = 0.0;// 今日匹配率 %
-};
-
-SyncStats db_get_sync_stats();
-
 
-#endif
+#endif

+ 1 - 13
src/feishu_client.cpp

@@ -4,8 +4,6 @@
 #include "feishu_client.h"
 #include "network_client.h"
 #include <iostream>
-#include <iomanip>
-#include <sstream>
 
 bool send_feishu_msg(
 	const std::string& token,
@@ -17,8 +15,7 @@ bool send_feishu_msg(
 	const std::string& dType,
 	const std::string& platenumcolor,
 	const std::string& vehicleType,
-	const std::string& tbNo,
-	double weight_kg  // fix27: 重量(kg),-1表示无重量数据
+	const std::string& tbNo
 ) {
 	std::string msg_buf;
 	msg_buf += "工程名:    " + project_nameEx + "\n";
@@ -31,14 +28,6 @@ bool send_feishu_msg(
 	msg_buf += "车型:         " + vehicleType + "\n";
 	msg_buf += "三联单编号:\n" + tbNo;
 
-	// fix27: 当 flagWeight=1 且重量有效时,附加重量信息
-	if (g_flag_weight && weight_kg >= 0) {
-		std::ostringstream oss;
-		oss << std::fixed << std::setprecision(1) << (weight_kg / 1000.0);
-		std::string weight_ton = oss.str();
-		msg_buf += "\n重量:       " + std::to_string((int)weight_kg) + " kg (" + weight_ton + " 吨)";
-	}
-
 	cJSON* content = cJSON_CreateObject();
 	cJSON_AddStringToObject(content, "text", msg_buf.c_str());
 	char* content_str = cJSON_PrintUnformatted(content);
@@ -126,7 +115,6 @@ 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_feishu_enabled) return false;  // fix27: 飞书未启用时不发送任何消息
     if (g_suppress_emergency_alert) return false;  // ✅ FIX9-1: WarningSigns=1抑制紧急告警
     std::string token = get_cached_tenant_token();
     if (token.empty()) {

+ 1 - 2
src/feishu_client.h

@@ -10,7 +10,6 @@ bool send_feishu_msg(const std::string& token,
     const std::string& dType,
     const std::string& platenumcolor,
     const std::string& vehicleType,
-    const std::string& tbNo,
-    double weight_kg = -1.0);  // fix27: 重量(kg),-1表示无重量数据
+    const std::string& tbNo);
 bool send_permanent_failure_alert(const std::string& plate, bool is_in, int retry_count, int db_id);
 #endif

+ 17 - 158
src/main.cpp

@@ -24,7 +24,6 @@
 #include "auth_middleware.h"
 #include "log_manager.h"
 #include "system_metrics_manager.h"
-#include "weight_dispatcher.h"  // fix24 v43: 并行称重调度器
 #include <iostream>
 
 int main(int argc, char** argv)
@@ -164,19 +163,6 @@ int main(int argc, char** argv)
 	load_station_cache_from_db();
 	//load_special_plates(pathComm + "config.txt");
 
-	// fix24 v41: 启动时清理孤儿照片路径
-	// 检查DB中 lo_photo_path/hi_photo_path 对应的文件是否还在 PlateJPG/ 目录,
-	// 文件已不存在的清空路径字段,避免前端显示 broken image 和 /photo 接口报错
-	{
-		std::string photo_dir = pathComm + "PlateJPG/";
-		struct stat st;
-		if (stat(photo_dir.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) {
-			std::cout << "[启动清理] 检查孤儿照片路径..." << std::endl;
-			int purged = db_purge_missing_photo_paths(photo_dir);
-			std::cout << "[启动清理] 完成,清空 " << purged << " 个孤儿路径" << std::endl;
-		}
-	}
-
 	if (hyperlpr_lib_init(model_path) != 0) {
 		std::cerr << "HyperLPR初始化失败,程序退出" << std::endl;
 		close_database();
@@ -309,7 +295,6 @@ int main(int argc, char** argv)
 	mqtt_client_init();
 	weight_scale_init();
 	start_weight_mqtt_worker();
-	weight_dispatcher_init();  // fix24 v43: 并行称重调度器
 
 	time_t last_online_report = 0;
 	time_t last_cleanup = 0;
@@ -365,8 +350,9 @@ int main(int argc, char** argv)
 				
 				if (plate_info->retry_count >= MAX_RETRY_COUNT) {
 					std::cerr << "[永久失败] " << plate_info->code << " 已重试" << MAX_RETRY_COUNT << "次,放弃" << std::endl;
+					// fix27: 空指针保护
 					send_permanent_failure_alert(plate_info->code,
-						plate_info->cap_info_copy->dtype == STATION_IN,
+						(plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN),
 						plate_info->retry_count, plate_info->db_id);
 					continue;
 				}
@@ -397,69 +383,15 @@ int main(int argc, char** argv)
 				// 启动上传线程
 				std::thread upload_thread([plate_info]() {
 					g_active_detach_threads++;
-					
-					// ✅ fix24 v43: 重试路径也需要确保重量任务已派发
-					if (g_sync_parallel_enabled && !plate_info->tb_num.empty()) {
-						// tb_num 已有(首次尝试已创建工单)
-						// weight_dispatch_async 内部有 tb_num 去重,已派发过的不会重复派发
-						// 此处补调一次,覆盖首次因 Bug#14/15 导致未派发的情况
-						int st_v43 = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN) ? 1 : 2;
-						std::cout << "[v43-重试] 确认重量任务已派发: "
-						          << plate_info->tb_num << std::endl;
-						weight_dispatch_async(plate_info->code, plate_info->tb_num, st_v43);
-					} else if (g_sync_parallel_enabled && plate_info->tb_num.empty()) {
-						// 首次尝试提前创建工单失败导致 tb_num 为空,重试时再尝试创建
-						bool is_in = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN);
-						if (check_bill_allowed(plate_info->code, is_in)) {
-							int res = lib_curl_create_bill_request(plate_info, plate_info->tb_num);
-							if (res == static_cast<int>(ErrorCode::SUCCESS) && !plate_info->tb_num.empty()) {
-								std::cout << "[v43-重试] 补创建工单成功: " << plate_info->code
-								          << " tb_num=" << plate_info->tb_num << std::endl;
-								int st_v43 = is_in ? 1 : 2;
-								weight_dispatch_async(plate_info->code, plate_info->tb_num, st_v43);
-							}
-						}
-					}
-					
-					// ✅ fix24 v43 Bug#15: 记录上传前状态,用于判断是否需要补发
-					bool tb_num_was_filled_before_retry_upload = g_sync_parallel_enabled && !plate_info->tb_num.empty();
-					
 					int result = plate_cap_info_upload(plate_info);
 					
 					if (result == static_cast<int>(ErrorCode::SUCCESS)) {
 						std::cout << "✅ " << plate_info->code << " 重试成功" << std::endl;
-						
-						// ✅ fix24 v43 Bug#15: plate_cap_info_upload 内部可能新填充了 tb_num
-						if (g_sync_parallel_enabled && !plate_info->tb_num.empty() && !tb_num_was_filled_before_retry_upload) {
-							int st_v43 = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN) ? 1 : 2;
-							std::cout << "[v43-重试-Bug#15] 上传内创建工单成功,补发重量任务: " << plate_info->code
-							          << " tb_num=" << plate_info->tb_num << std::endl;
-							weight_dispatch_async(plate_info->code, plate_info->tb_num, st_v43);
-						}
-						
-						// ✅ fix24 v43: 重试成功后也需要等待重量结果(v43 路径)
-						if (g_sync_parallel_enabled && !plate_info->tb_num.empty()) {
-							WeightDispatchResult wresult;
-							bool got_weight = weight_dispatch_wait_result(
-								plate_info->tb_num, g_sync_reporter_timeout_sec, wresult);
-							if (got_weight) {
-								std::cout << "[v43-重试-Reporter] 两路汇聚完成: " << plate_info->code
-								          << " 联单:" << plate_info->tb_num
-								          << " 重量:" << wresult.weight_kg << "kg" << std::endl;
-							} else {
-								std::cerr << "[v43-重试-Reporter] 等待重量超时: "
-								          << plate_info->code << std::endl;
-							}
-							// fix27 Bug#31: 所有消费者已获取结果,清理 entry 防止内存泄漏
-							weight_dispatch_cleanup(plate_info->tb_num);
-						}
+						// ✅ 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;
-						// ✅ fix24 v43: 清理可能未消费的重量 entry
-						if (g_sync_parallel_enabled && !plate_info->tb_num.empty()) {
-							weight_dispatch_cleanup(plate_info->tb_num);
-						}
 					} else {
 						// 网络错误,重新加入重试队列
 						std::lock_guard<std::mutex> lock(g_retry_queue_mtx);
@@ -668,114 +600,39 @@ int main(int argc, char** argv)
 				continue;
 			}
 
-			// ✅ fix24 v43: 重量任务在 upload_thread 内部派发(工单创建后立即派发)
-			// 原因:tb_num 由 plate_cap_info_upload 内部的 API 调用填充,
-			// 在 save_file 后 tb_num 仍为空,此处派发永远不触发(Bug#14 修复)
-
 			// ✅ fix18-P0: 设置上传进行中标志,防止主循环peek竞态
 			plate_info->upload_in_progress.store(true);
 
 			// 异步处理上传
 			std::thread upload_thread([plate_info]() {
 				g_active_detach_threads++;
-				
-				// ✅ fix24 v43 Bug#14: 在照片上传前先创建工单获得 tb_num,再派发重量任务
-				// 原 Bug:tb_num 在 plate_cap_info_upload 内部才由 API 填充,
-				// 但 weight_dispatch_async 在此线程外检查 tb_num,此时永远为空
-				if (g_sync_parallel_enabled) {
-					if (plate_info->tb_num.empty()) {
-						bool is_in = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN);
-						if (!check_bill_allowed(plate_info->code, is_in)) {
-							g_metrics.record_createbill(false, false, true);
-							std::cerr << "[v43] 提前创建工单被配额拦截: " << plate_info->code << std::endl;
-						} else {
-							int res = lib_curl_create_bill_request(plate_info, plate_info->tb_num);
-							if (res != static_cast<int>(ErrorCode::SUCCESS)) {
-								std::cerr << "[v43] 提前创建工单失败: " << plate_info->code
-								          << " 错误码:" << res << std::endl;
-								if (res == static_cast<int>(ErrorCode::QUOTA_EXCEEDED)) {
-									g_metrics.record_createbill(false, false, true);
-								}
-								// tb_num 为空时不派发重量,继续走 plate_cap_info_upload 的正常错误处理
-							} else {
-								std::cout << "[v43] 提前创建工单成功: " << plate_info->code
-								          << " tb_num=" << plate_info->tb_num << std::endl;
-							}
-						}
-					}
-					// tb_num 已就绪(提前创建 或 缓存命中),派发重量任务
-					if (!plate_info->tb_num.empty()) {
-						int st_v43 = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN) ? 1 : 2;
-						std::cout << "[v43] tb_num=" << plate_info->tb_num << " 确定,派发重量任务(并行模式)" << std::endl;
-						weight_dispatch_async(plate_info->code, plate_info->tb_num, st_v43);
-					}
-				}
-				
-				// ✅ fix24 v43 Bug#15: 记录进入上传前 tb_num 是否已填充
-				// 若上传前为空但上传后被 plate_cap_info_upload 内部填充,需补发重量任务
-				bool tb_num_was_filled_before_upload = g_sync_parallel_enabled && !plate_info->tb_num.empty();
-				
 				int result = plate_cap_info_upload(plate_info);
 				
 				if (result == static_cast<int>(ErrorCode::SUCCESS)) {
 					std::cout << "✅ " << plate_info->code << " 处理完成" << std::endl;
-					
-					// ✅ fix24 v43 Bug#15: 提前创建工单失败但 plate_cap_info_upload 内部成功时,
-					// tb_num 被填充但重量任务从未派发,需在此补发
-					if (g_sync_parallel_enabled && !plate_info->tb_num.empty() && !tb_num_was_filled_before_upload) {
-						int st_v43 = (plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN) ? 1 : 2;
-						std::cout << "[v43-Bug#15] 上传内创建工单成功,补发重量任务: " << plate_info->code
-						          << " tb_num=" << plate_info->tb_num << std::endl;
-						weight_dispatch_async(plate_info->code, plate_info->tb_num, st_v43);
-					}
-					
-					if (g_sync_parallel_enabled && !plate_info->tb_num.empty()) {
-						// ===== v43 路径:等待重量结果 =====
-						WeightDispatchResult wresult;
-						bool got_weight = weight_dispatch_wait_result(
-							plate_info->tb_num, g_sync_reporter_timeout_sec, wresult);
-						
-						if (got_weight) {
-							std::cout << "[v43-Reporter] 两路汇聚完成: " << plate_info->code
-							          << " 联单:" << plate_info->tb_num
-							          << " 重量:" << wresult.weight_kg << "kg"
-							          << " 上传:" << (wresult.upload_status == 1 ? "✅" : "❌") << std::endl;
-						} else {
-							std::cerr << "[v43-Reporter] 等待重量超时(" << g_sync_reporter_timeout_sec
-							          << "s): " << plate_info->code << " 联单:" << plate_info->tb_num << std::endl;
-						}
-						// fix27 Bug#31: 所有消费者已获取结果,清理 entry 防止内存泄漏
-						weight_dispatch_cleanup(plate_info->tb_num);
-					} else {
-						// ===== v41 路径:照片成功后提交称重+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);
-						}
+					// ✅ 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;
-					// v43: 照片失败不影响重量(重量已在并行执行)
-					if (g_sync_parallel_enabled && !plate_info->tb_num.empty()) {
-						std::cout << "[v43] 照片上传失败,但重量任务已独立执行,不受影响" << std::endl;
-						// ✅ fix24 v43 Bug#12: 清理无人消费的重量任务 entry,防止 map 泄漏
-						weight_dispatch_cleanup(plate_info->tb_num);
-					}
 				} 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();
-					// ✅ fix27: 添加空指针检查,防止 cap_info_copy 为空时崩溃
+					// fix27: 空指针保护
 					send_permanent_failure_alert(plate_info->code, 
 						(plate_info->cap_info_copy && plate_info->cap_info_copy->dtype == STATION_IN), 
 						plate_info->retry_count, plate_info->db_id);
-					// ✅ fix24 v43 Bug#12: 清理无人消费的重量任务 entry
-					if (g_sync_parallel_enabled && !plate_info->tb_num.empty()) {
-						weight_dispatch_cleanup(plate_info->tb_num);
-					}
 				} 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;
@@ -785,12 +642,15 @@ int main(int argc, char** argv)
 						          << "秒后重试" << 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--;
@@ -923,7 +783,6 @@ int main(int argc, char** argv)
 	// ✅ fix24-v37: 停止系统监控历史数据采集
 	MetricsManager::instance().stop();
 
-	weight_dispatcher_shutdown();  // fix24 v43: 等待重量任务完成
 	stop_weight_mqtt_worker();
 	weight_scale_close();
 	mqtt_client_close();

+ 1 - 3
src/network_client.cpp

@@ -27,9 +27,7 @@ int parse_create_bill_response(const std::string& response, const std::string& p
         std::cerr << "[解析失败] 响应不是合法JSON: " << response << std::endl;
         return static_cast<int>(ErrorCode::PARSE_ERROR);
     }
-    if (DEBUG_LOG) {
-        std::cout << response << std::endl;
-    }
+    if (DEBUG_LOG) { std::cout << response << std::endl; }
   
     cJSON* status = cJSON_GetObjectItem(root, "status");
     cJSON* data = cJSON_GetObjectItem(root, "data");

+ 2 - 33
src/plate_recognizer.cpp

@@ -76,38 +76,8 @@ cv::Mat create_text_watermark(const cv::Mat& src_image, std::string text)
 	size_t font_size = 38;
 	cvx::CvxFont font(pathComm + "simsun.ttc");
 	cv::Mat watermark(160, width, CV_8UC3, cv::Scalar(0, 0, 0));
-
-	// 测量文字宽度,计算居中位置(修复:文字超出右边界被截断)
-	cv::Scalar font_color(255, 255, 255);
-	int baseline = 0;
-	cv::Size text_size1 = cv::getTextSize(firstHalf, cv::FONT_HERSHEY_SIMPLEX, font_size / 28.0, 1, &baseline);
-	cv::Size text_size2 = cv::getTextSize(secondHalf, cv::FONT_HERSHEY_SIMPLEX, font_size / 28.0, 1, &baseline);
-
-	// 估算中文字符宽度(cv::getTextSize 对 FreeType 字体不准确,用字符数估算)
-	// 中文字符宽度 ≈ font_size,英文字符宽度 ≈ font_size / 2
-	auto estimate_text_width = [](const std::string& s, size_t fs) -> int {
-		int w = 0;
-		for (size_t i = 0; i < s.length(); ) {
-			int bytes = getCharBytes((unsigned char)s[i]);
-			if (bytes >= 3) w += fs;       // 中文字符(UTF-8 3字节)
-			else if (bytes == 2) w += fs;  // 特殊字符
-			else w += fs / 2;              // ASCII字符
-			i += bytes;
-		}
-		return w;
-	};
-
-	int text_width1 = estimate_text_width(firstHalf, font_size);
-	int text_width2 = estimate_text_width(secondHalf, font_size);
-
-	// 居中位置:x = (总宽度 - 文字宽度) / 2,向左偏移半个中文字符宽度
-	// fix27: FreeType 实际渲染宽度比估算值略宽,导致右侧截断,偏移修正
-	int half_char = font_size / 2;  // 约半个中文字符宽度(19px)
-	int x1 = std::max(0, (width - text_width1) / 2 - half_char);
-	int x2 = std::max(0, (width - text_width2) / 2 - half_char);
-
-	putText(watermark, firstHalf, cv::Point(x1, 80), font, font_size, font_color);
-	putText(watermark, secondHalf, cv::Point(x2, 140), font, font_size, font_color);
+	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;
 }
 
@@ -407,7 +377,6 @@ int hyperlpr_lib_update(P_HLPR_DataBuffer* buffer, cv::Mat image, cv::Mat image_
 				plate_info->first_seen_time = time(NULL);
                 plate_info->db_id = 0; // 初始化数据库ID为0
                 // ✅ v41.1 新增:设置明确的方向标记
-                // ✅ fix27 Bug#33: 使用 effective_cap_info 而非原始 cap_info 判断方向
                 plate_info->is_inbound = (effective_cap_info->dtype == STATION_IN);
                 plate_info->is_outbound = (effective_cap_info->dtype == STATION_OUT);
 				plate_rec_app.map.insert({ map_key, plate_info });

+ 0 - 785
src/plate_recognizer1.cpp

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

+ 2 - 44
src/station_lock.cpp

@@ -43,11 +43,6 @@
 #include "database.h"
 #include <iostream>
 
-// ✅ v40修复:出站无记录拦截追踪(用于告警,不自动放行)
-// 无进站记录出站保持严格拦截(fix16),仅追踪拦截时长用于告警通知
-static std::map<std::string, time_t> g_norecord_block_start;
-static const int NORECORD_ALERT_SEC = 600;  // 无记录拦截超过10分钟触发告警
-
 static void _save_station_cache_to_db_unlocked(const std::string& plate, const PlateStationState& state) {
     if (!g_db) return;
     
@@ -224,9 +219,6 @@ bool try_lock_in_station(const std::string& plate) {
         std::lock_guard<std::mutex> lock_db(g_db_mtx);
         _save_station_cache_to_db_unlocked(plate, state);
         
-        // ✅ v40: 清除无记录拦截追踪(车辆已有进站记录)
-        g_norecord_block_start.erase(plate);
-        
         g_metrics.record_station_lock(true);
         std::cout << "[进站锁定] " << plate << " 首次进站,需等待" 
                   << g_in_out_interval_sec << "秒后才允许出站" << std::endl;
@@ -411,25 +403,7 @@ bool peek_station_lock(const std::string& plate, bool is_in) {
         if (is_in) {
             return true;  // 首次进站允许
         } else {
-            // ✅ fix16: 出站无进站记录严格拦截(必须先进站才能出站,不自动放行)
-            // ✅ v40: 追踪拦截时长,超10分钟触发告警通知管理员手动处理
-            auto block_it = g_norecord_block_start.find(plate);
-            if (block_it == g_norecord_block_start.end()) {
-                g_norecord_block_start[plate] = now;
-                std::cout << "[出站拦截-无记录] " << plate 
-                          << " 无进站记录,必须先进站才能出站" << std::endl;
-                return false;
-            }
-            int blocked_elapsed = static_cast<int>(difftime(now, block_it->second));
-            if (blocked_elapsed >= NORECORD_ALERT_SEC && blocked_elapsed % NORECORD_ALERT_SEC < 35) {
-                // 每10分钟重复告警一次
-                std::cerr << "[⚠️出站告警-无记录] " << plate 
-                          << " 无进站记录已拦截" << blocked_elapsed << "秒"
-                          << ",需管理员手动处理(Web页面清除锁定或确认车辆已进站)" << std::endl;
-            } else {
-                std::cout << "[出站拦截-无记录] " << plate 
-                          << " 无进站记录,已拦截" << blocked_elapsed << "秒" << std::endl;
-            }
+            // ✅ fix16: 出站无进站记录永远返回false(必须先进站才能出站)
             return false;
         }
     }
@@ -600,16 +574,6 @@ void cleanup_station_cache() {
         }
         std::cout << "[锁定清零] 清除 " << expired_plates.size() << " 条超过2小时的锁定记录" << std::endl;
     }
-    
-    // ✅ v40: 同步清理无记录拦截追踪中已无意义的条目(超30分钟)
-    for (auto it = g_norecord_block_start.begin(); it != g_norecord_block_start.end(); ) {
-        int elapsed = static_cast<int>(difftime(now, it->second));
-        if (elapsed >= 1800) {
-            it = g_norecord_block_start.erase(it);
-        } else {
-            ++it;
-        }
-    }
 }
 
 // ==================== 交替锁定核心函数结束 ====================
@@ -633,9 +597,6 @@ bool clear_station_lock(const std::string& plate) {
     
     g_plate_station_cache.erase(it);
     
-    // ✅ v40: 同步清除无记录拦截追踪
-    g_norecord_block_start.erase(plate);
-    
     // 同步删除数据库记录
     std::lock_guard<std::mutex> lock_db(g_db_mtx);
     if (g_db) {
@@ -657,10 +618,7 @@ int get_remaining_wait_time(const std::string& plate, bool is_in) {
     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()) {
-        // 无记录:进站允许(返回0),出站永久拦截(返回-1)
-        return is_in ? 0 : -1;
-    }
+    if (it == g_plate_station_cache.end()) return 0;  // 无记录,可操作
     
     const PlateStationState& state = it->second;
     time_t now = time(NULL);

+ 0 - 9
src/utils.cpp

@@ -1065,24 +1065,15 @@ void daily_file_cleanup() {
     // 从最旧的文件开始删除,直到总大小降至阈值以下
     int deleted_count = 0;
     long long freed_bytes = 0;
-    std::vector<std::string> deleted_paths;  // fix24 v41: 收集被删除的路径,用于同步DB
-    
     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++;
-            deleted_paths.push_back(f.first);  // fix24 v41: 记录成功删除的路径
         }
     }
 
-    // fix24 v41: 同步清空数据库中的照片路径字段
-    if (!deleted_paths.empty()) {
-        int db_updated = db_clear_photo_paths_by_paths(deleted_paths);
-        std::cout << "[照片清理] 同步清空DB路径字段: " << db_updated << " 个字段已更新" << std::endl;
-    }
-
     // 清理空子目录
     remove_empty_dirs(photo_dir);
 

+ 43 - 337
src/web_server.cpp

@@ -8,7 +8,6 @@
 #include "network_client.h"
 #include "feishu_client.h"
 #include "weight_scale.h"
-#include "weight_dispatcher.h"  // fix27 Bug#28: 使用 promise/future 等待重量结果
 #include "utils.h"
 #include "rtsp_capture.h"
 #include "auth_api.h"
@@ -206,58 +205,6 @@ std::string generate_message_list_json(int page, const std::string& search_keywo
 	return json.str();
 }
 
-// ==================== fix24 v41: 分类查询 JSON(带 total/total_pages) ====================
-std::string generate_message_list_json_v2(int page, int page_size, const RecordQuery& q) {
-	if (page < 1) page = 1;
-	if (page_size < 1) page_size = 20;
-	if (page_size > 200) page_size = 200;
-
-	int total = db_count_records_advanced(q);
-	std::vector<DbRecord> records = db_get_records_advanced(page, page_size, q);
-	int total_pages = (total + page_size - 1) / page_size;
-
-	std::ostringstream json;
-	json << "{\"page\":" << page
-	     << ",\"page_size\":" << page_size
-	     << ",\"total\":" << total
-	     << ",\"total_pages\":" << total_pages
-	     << ",\"records\":[";
-
-	bool first = true;
-	for (const auto& rec : records) {
-		if (!first) json << ",";
-		first = false;
-
-		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 << "{"
-		     << "\"id\":" << rec.id << ","
-		     << "\"create_time\":\"" << escape_html(display_time) << "\","
-		     << "\"capture_time\":" << rec.capture_time << ","
-		     << "\"plate_number\":\"" << escape_html(rec.plate_number) << "\","
-		     << "\"tb_num\":\"" << escape_html(rec.tb_num) << "\","
-		     << "\"station_type\":" << rec.station_type << ","
-		     << "\"lo_photo_path\":\"" << escape_html(rec.lo_photo_path) << "\","
-		     << "\"hi_photo_path\":\"" << escape_html(rec.hi_photo_path) << "\","
-		     << "\"lo_upload_status\":" << rec.lo_upload_status << ","
-		     << "\"hi_upload_status\":" << rec.hi_upload_status << ","
-		     << "\"feishu_status\":" << rec.feishu_status << ","
-		     << "\"retry_count\":" << rec.retry_count
-		     << "}";
-	}
-	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;
@@ -265,28 +212,20 @@ bool is_safe_file_path(const std::string& base_dir, const std::string& user_path
     }
 
     std::string full_path = base_dir + "/" + user_path;
-
-    // fix24 v41: 先检查文件是否存在(照片可能已被清理),不存在时静默返回 false
-    // 不再打 ERROR 日志,避免每次刷新页面都刷屏
-    struct stat st;
-    if (stat(full_path.c_str(), &st) != 0) {
-        return false;
-    }
-
+    
     char abs_path[PATH_MAX];
     if (realpath(full_path.c_str(), abs_path) == nullptr) {
-        // realpath 失败但 stat 成功(理论上不应发生),降级使用拼接路径
-        real_path = full_path;
-    } else {
-        real_path = abs_path;
+        std::cerr << "[路径校验] 转换绝对路径失败: " << full_path << " - " << strerror(errno) << std::endl;
+        return false;
     }
+    real_path = abs_path;
 
-    char base_abs[PATH_MAX];
-    if (realpath(base_dir.c_str(), base_abs) == nullptr) {
+    std::string base_abs_path;
+    if (realpath(base_dir.c_str(), abs_path) == nullptr) {
         std::cerr << "[路径校验] 基础目录不存在: " << base_dir << std::endl;
         return false;
     }
-    std::string base_abs_path = base_abs;
+    base_abs_path = abs_path;
     if (base_abs_path.back() != '/') base_abs_path += '/';
 
     if (real_path.find(base_abs_path) != 0) {
@@ -510,22 +449,22 @@ void start_web_server() {
 	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) {
+		// fix27: 使用 cJSON 安全解析
+		cJSON* root = cJSON_Parse(req.body.c_str());
+		if (!root) {
 			res.status = 400;
-			res.set_content("{\"success\":false,\"error\":\"缺少 key 字段\"}", "application/json");
+			res.set_content("{\"success\":false,\"error\":\"JSON 解析失败\"}", "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) {
+		cJSON* key_item = cJSON_GetObjectItem(root, "key");
+		if (!key_item || !cJSON_IsString(key_item) || !key_item->valuestring) {
+			cJSON_Delete(root);
 			res.status = 400;
-			res.set_content("{\"success\":false,\"error\":\"key 值解析失败\"}", "application/json");
+			res.set_content("{\"success\":false,\"error\":\"缺少 key 字段\"}", "application/json");
 			return;
 		}
-		std::string key_value = req.body.substr(q1 + 1, q2 - q1 - 1);
+		std::string key_value = key_item->valuestring;
+		cJSON_Delete(root);
 
 		if (ssh_mgr::add_key(key_value, error)) {
 			res.set_content("{\"success\":true,\"message\":\"公钥已添加\"}", "application/json");
@@ -683,10 +622,6 @@ void start_web_server() {
 		j << "," << check_rtsp("侧面出站", rtsp_url_side_out, plate_rec_app.capture_side_out);
 		j << "],";
 
-		// --- 飞书 ---
-		j << "\"feishu\":{\"enabled\":" << (g_feishu_enabled ? "true" : "false")
-		  << ",\"chat_id\":\"" << g_feishu_chat_id << "\"},";
-
 		// --- MQTT ---
 		int mqtt_st = mqtt_get_status();
 		const char* mqtt_state_str[] = {"未启用", "已连接", "连接中", "已断开"};
@@ -811,55 +746,17 @@ void start_web_server() {
 
 	svr.Get("/api/messages", [](const httplib::Request& req, httplib::Response& res) {
 		int page = 1;
-		int page_size = 20;
-		auto getParam = [&](const char* key, std::string& out) {
-			auto it = req.params.find(key);
-			if (it != req.params.end()) out = it->second;
-		};
+		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 ps_it = req.params.find("page_size");
-		if (ps_it != req.params.end()) {
-			page_size = std::atoi(ps_it->second.c_str());
-			if (page_size < 1) page_size = 20;
-			if (page_size > 200) page_size = 200;
-		}
-
-		// fix24 v41: 分类查询参数
-		RecordQuery q;
-		getParam("plate", q.plate_number);
-		getParam("tb_num", q.tb_num);
-		getParam("date_from", q.date_from);
-		getParam("date_to", q.date_to);
-		auto st_it = req.params.find("station_type");
-		if (st_it != req.params.end()) q.station_type = std::atoi(st_it->second.c_str());
-		auto us_it = req.params.find("upload_status");
-		if (us_it != req.params.end()) q.upload_status = std::atoi(us_it->second.c_str());
-		auto fs_it = req.params.find("feishu_status");
-		if (fs_it != req.params.end()) q.feishu_status = std::atoi(fs_it->second.c_str());
-
-		// 兼容旧 keyword 参数:若未传分类参数则用 keyword 模糊匹配
-		std::string keyword;
-		getParam("keyword", keyword);
-		bool has_advanced =
-			!q.plate_number.empty() || !q.tb_num.empty() ||
-			!q.date_from.empty() || !q.date_to.empty() ||
-			q.station_type != 0 || q.upload_status != 0 || q.feishu_status != 0;
-
-		std::string json;
-		if (has_advanced) {
-			json = generate_message_list_json_v2(page, page_size, q);
-		} else if (!keyword.empty()) {
-			// 旧版 keyword 兼容:走旧函数保持 plate/tb_num/date 三字段模糊查询
-			json = generate_message_list_json(page, keyword);
-			res.set_content(json, "application/json; charset=utf-8");
-			return;
-		} else {
-			json = generate_message_list_json_v2(page, page_size, q);
+		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");
 		});
 
@@ -1131,75 +1028,21 @@ void start_web_server() {
 	// ✅ 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());
-			if (page < 1) page = 1;
-		}
+		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());
-			if (page_size < 1) page_size = 20;
-			if (page_size > 200) page_size = 200;
-		}
-		// fix24 v41: 支持 page_size 参数别名
-		it = req.params.find("page_size");
-		if (it != req.params.end()) {
-			page_size = std::atoi(it->second.c_str());
-			if (page_size < 1) page_size = 20;
-			if (page_size > 200) page_size = 200;
-		}
-
-		// fix24 v41: 分类查询参数
-		WeightQuery wq;
-		bool has_advanced = false;
-
-		it = req.params.find("plate");
-		if (it != req.params.end() && !it->second.empty()) { wq.plate_number = it->second; has_advanced = true; }
-		it = req.params.find("tb_num");
-		if (it != req.params.end() && !it->second.empty()) { wq.tb_num = it->second; has_advanced = true; }
-		it = req.params.find("date_from");
-		if (it != req.params.end() && !it->second.empty()) { wq.date_from = it->second; has_advanced = true; }
-		it = req.params.find("date_to");
-		if (it != req.params.end() && !it->second.empty()) { wq.date_to = it->second; has_advanced = true; }
-		it = req.params.find("station_type");
-		if (it != req.params.end()) {
-			wq.station_type = std::atoi(it->second.c_str());
-			if (wq.station_type != 0) has_advanced = true;
-		}
-		it = req.params.find("upload_status");
-		if (it != req.params.end()) {
-			wq.upload_status = std::atoi(it->second.c_str());
-			if (wq.upload_status != 0) has_advanced = true;
-		}
-
-		// 兼容旧 keyword 参数
-		std::string keyword;
+		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;
 
-		std::vector<DbRecord> records;
-		int total = 0;
+		auto records = db_get_weight_records(page, page_size, keyword);
+		int total = db_get_weight_total_count();
 		int failed = db_get_weight_failed_count();
 
-		if (has_advanced) {
-			records = db_get_weight_records_advanced(page, page_size, wq);
-			total = db_count_weight_records_advanced(wq);
-		} else if (!keyword.empty()) {
-			// 旧版 keyword 兼容:OR 匹配 plate_number 或 tb_num,回退到旧函数
-			records = db_get_weight_records(page, page_size, keyword);
-			total = db_get_weight_total_count();
-		} else {
-			records = db_get_weight_records(page, page_size, "");
-			total = db_get_weight_total_count();
-		}
-
-		int total_pages = (total + page_size - 1) / page_size;
-
 		std::ostringstream json;
 		json << "{\"page\":" << page << ",\"page_size\":" << page_size
-			 << ",\"total\":" << total << ",\"total_pages\":" << total_pages
-			 << ",\"failed\":" << failed
+			 << ",\"total\":" << total << ",\"failed\":" << failed
 			 << ",\"records\":[";
 		for (size_t i = 0; i < records.size(); i++) {
 			const auto& rec = records[i];
@@ -1264,94 +1107,6 @@ void start_web_server() {
 		res.set_content(json.str(), "application/json; charset=utf-8");
 	});
 
-	// ==================== fix24 v43 P2: 同步状态统计 API ====================
-	svr.Get("/api/sync/stats", [](const httplib::Request& req, httplib::Response& res) {
-		SyncStats stats = db_get_sync_stats();
-
-		// 获取当前同步模式
-		bool parallel_mode = g_sync_parallel_enabled;
-
-		std::ostringstream json;
-		json << "{"
-		     << "\"parallel_mode\":" << (parallel_mode ? "true" : "false")
-		     << ",\"total_uploads\":" << stats.total_uploads
-		     << ",\"today_uploads\":" << stats.today_uploads
-		     << ",\"matched_weight\":" << stats.matched_weight
-		     << ",\"today_matched\":" << stats.today_matched
-		     << ",\"weight_upload_ok\":" << stats.weight_upload_ok
-		     << ",\"weight_upload_fail\":" << stats.weight_upload_fail
-		     << ",\"weight_pending\":" << stats.weight_pending
-		     << ",\"match_rate\":" << std::fixed << std::setprecision(1) << stats.match_rate
-		     << ",\"today_match_rate\":" << std::fixed << std::setprecision(1) << stats.today_match_rate
-		     << "}";
-		res.set_content(json.str(), "application/json; charset=utf-8");
-	});
-
-	// ==================== fix24 v43 P2: 两表联合查询 API ====================
-	svr.Get("/api/sync/events", [](const httplib::Request& req, httplib::Response& res) {
-		int page = 1, page_size = 20;
-		auto it = req.params.find("page");
-		if (it != req.params.end()) {
-			page = std::atoi(it->second.c_str());
-			if (page < 1) page = 1;
-		}
-		it = req.params.find("page_size");
-		if (it != req.params.end()) {
-			page_size = std::atoi(it->second.c_str());
-			if (page_size < 1) page_size = 20;
-			if (page_size > 200) page_size = 200;
-		}
-
-		SyncQuery sq;
-		it = req.params.find("plate");
-		if (it != req.params.end() && !it->second.empty()) sq.plate_number = it->second;
-		it = req.params.find("tb_num");
-		if (it != req.params.end() && !it->second.empty()) sq.tb_num = it->second;
-		it = req.params.find("date_from");
-		if (it != req.params.end() && !it->second.empty()) sq.date_from = it->second;
-		it = req.params.find("date_to");
-		if (it != req.params.end() && !it->second.empty()) sq.date_to = it->second;
-		it = req.params.find("station_type");
-		if (it != req.params.end()) sq.station_type = std::atoi(it->second.c_str());
-		it = req.params.find("sync_status");
-		if (it != req.params.end()) sq.sync_status = std::atoi(it->second.c_str());
-
-		int total = db_count_event_records_joined(sq);
-		auto records = db_get_event_records_joined(page, page_size, sq);
-		int total_pages = (total + page_size - 1) / page_size;
-		if (total_pages < 1) total_pages = 1;
-
-		std::ostringstream json;
-		json << "{\"page\":" << page
-		     << ",\"page_size\":" << page_size
-		     << ",\"total\":" << total
-		     << ",\"total_pages\":" << total_pages
-		     << ",\"records\":[";
-		for (size_t i = 0; i < records.size(); i++) {
-			const auto& rec = records[i];
-			if (i > 0) json << ",";
-			json << "{"
-			     << "\"ur_id\":" << rec.ur_id
-			     << ",\"plate_number\":\"" << escape_html(rec.plate_number) << "\""
-			     << ",\"tb_num\":\"" << escape_html(rec.tb_num) << "\""
-			     << ",\"station_type\":" << rec.station_type
-			     << ",\"create_time\":\"" << escape_html(rec.create_time) << "\""
-			     << ",\"capture_time\":\"" << escape_html(rec.capture_time) << "\""
-			     << ",\"lo_upload_status\":" << rec.lo_upload_status
-			     << ",\"hi_upload_status\":" << rec.hi_upload_status
-			     << ",\"feishu_status\":" << rec.feishu_status
-			     << ",\"wr_id\":" << rec.wr_id
-			     << ",\"weight_kg\":" << std::fixed << std::setprecision(1) << rec.weight_kg
-			     << ",\"weight_upload_status\":" << rec.weight_upload_status
-			     << ",\"weight_response_msg\":\"" << escape_html(rec.weight_response_msg) << "\""
-		     << ",\"synced\":" << (rec.wr_id > 0 ? "true" : "false")
-			     << "}";
-		}
-		json << "]}";
-		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()) {
@@ -1378,24 +1133,11 @@ void start_web_server() {
 		fseek(fp, 0, SEEK_END);
 		long file_size = ftell(fp);
 		fseek(fp, 0, SEEK_SET);
-		if (file_size <= 0 || file_size > 20L * 1024 * 1024) {
-			// 空文件或超过20MB,拒绝
-			fclose(fp);
-			res.status = 404;
-			res.set_content("文件无效", "text/plain; charset=utf-8");
-			return;
-		}
-
-		std::vector<char> buffer((size_t)file_size);
-		size_t rd = fread(buffer.data(), 1, (size_t)file_size, fp);
+		std::vector<char> buffer(file_size);
+		fread(buffer.data(), 1, file_size, fp);
 		fclose(fp);
-		if (rd != (size_t)file_size) {
-			res.status = 500;
-			res.set_content("读取文件失败", "text/plain; charset=utf-8");
-			return;
-		}
 
-		res.set_content(buffer.data(), (size_t)file_size, "image/jpeg");
+		res.set_content(buffer.data(), file_size, "image/jpeg");
 	});
 
 	// ==================== fix21: 配置页面与API ====================
@@ -2275,31 +2017,13 @@ PhotoUploadResult upload_in_photos(std::shared_ptr<CarPlateInfo> plate_info,
 				} else {
 					feishu_time_str = get_format_time();
 				}
-				// fix27 Bug#28: 使用 weight_dispatch_wait_result 直接等待重量结果(而非轮询数据库)
-				// 原方案 db_wait_weight_by_plate 只等3秒,但重量任务需要:读秤(最多8秒)+上传+写DB,3秒不够
-				// 新方案使用 promise/future 机制,直接等待重量任务完成
-				double weight_kg = -1.0;
-				if (g_weight_enabled && !tb_num.empty()) {
-					WeightDispatchResult wresult;
-					// 等待重量结果,超时15秒(覆盖读秤8秒+上传+写DB)
-					bool got_weight = weight_dispatch_wait_result(tb_num, 15, wresult);
-					if (got_weight && wresult.read_status == 1) {
-						weight_kg = wresult.weight_kg;
-						std::cout << "[进站] " << plate << " 获取到重量: " << weight_kg << " kg" << std::endl;
-					} else {
-						std::cout << "[进站] " << plate << " 等待重量超时或读取失败" << std::endl;
-					}
-				}
-				bool feishu_ok = false;
-				if (g_feishu_enabled) {
-					feishu_ok = send_feishu_msg(token,
-						project_name, plate, feishu_time_str,
-						point_number, throughway,
-						"进站", platenumcolor, vehicleType,
-						tb_num, weight_kg);
-				}
+				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, g_feishu_enabled ? (feishu_ok ? 1 : 2) : 0);
+					db_update_feishu_status(plate_info->db_id, feishu_ok ? 1 : 2);
 				}
 			}
 		}
@@ -2431,31 +2155,13 @@ PhotoUploadResult upload_out_photos(std::shared_ptr<CarPlateInfo> plate_info,
 				} else {
 					feishu_time_str = get_format_time();
 				}
-				// fix27 Bug#28: 使用 weight_dispatch_wait_result 直接等待重量结果(而非轮询数据库)
-				// 原方案 db_wait_weight_by_plate 只等3秒,但重量任务需要:读秤(最多8秒)+上传+写DB,3秒不够
-				// 新方案使用 promise/future 机制,直接等待重量任务完成
-				double weight_kg = -1.0;
-				if (g_weight_enabled && !tb_num.empty()) {
-					WeightDispatchResult wresult;
-					// 等待重量结果,超时15秒(覆盖读秤8秒+上传+写DB)
-					bool got_weight = weight_dispatch_wait_result(tb_num, 15, wresult);
-					if (got_weight && wresult.read_status == 1) {
-						weight_kg = wresult.weight_kg;
-						std::cout << "[出站] " << plate << " 获取到重量: " << weight_kg << " kg" << std::endl;
-					} else {
-						std::cout << "[出站] " << plate << " 等待重量超时或读取失败" << std::endl;
-					}
-				}
-				bool feishu_ok = false;
-				if (g_feishu_enabled) {
-					feishu_ok = send_feishu_msg(token,
-						project_name, plate, feishu_time_str,
-						point_number, throughway,
-						"出站", platenumcolor, vehicleType,
-						tb_num, weight_kg);
-				}
+				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, g_feishu_enabled ? (feishu_ok ? 1 : 2) : 0);
+					db_update_feishu_status(plate_info->db_id, feishu_ok ? 1 : 2);
 				}
 			}
 		}

+ 0 - 3
src/web_server.h

@@ -1,15 +1,12 @@
 #ifndef WEB_SERVER_H
 #define WEB_SERVER_H
 #include "common.h"
-#include "database.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);
-// fix24 v41: 分类查询 JSON(带 total/total_pages)
-std::string generate_message_list_json_v2(int page, int page_size, const RecordQuery& q);
 int manual_retry_record(int id);
 std::string escape_html(const std::string& str);
 PhotoUploadResult upload_in_photos(std::shared_ptr<CarPlateInfo> plate_info,

+ 0 - 351
src/weight_dispatcher.cpp

@@ -1,351 +0,0 @@
-/**
- * weight_dispatcher.cpp — v43 并行称重调度器实现
- * 
- * fix24 v43 核心:tb_num 确定即并行,重量不再等照片。
- * 
- * 调用链路(v43 enabled=1):
- *   main.cpp: save_file() → tb_num 确定
- *           → weight_dispatch_async(plate, tb_num, st)    [启动重量线程]
- *           → upload_thread: plate_cap_info_upload()       [照片上传]
- *           → upload_thread: weight_dispatch_wait_result()  [等待重量结果]
- *           → upload_thread: db_insert_event_atomic()      [原子写入两表]
- *           → upload_thread: mqtt_client_publish()          [MQTT 发布]
- */
-#include "weight_dispatcher.h"
-#include "weight_scale.h"
-#include "database.h"
-#include "mqtt_client.h"
-#include <future>
-#include <unordered_map>
-#include <mutex>
-#include <condition_variable>
-#include <thread>
-#include <atomic>
-#include <iostream>
-#include <string>
-
-// ==================== 外部依赖(来自 common.h / weight_scale.h) ====================
-extern bool g_weight_enabled;
-extern bool g_mqtt_enabled;
-extern double g_weight_threshold_in;
-extern double g_weight_threshold_out;
-
-// ==================== 配置全局变量(来自 common.h) ====================
-extern bool g_sync_parallel_enabled;    // [sync] enabled
-extern int  g_sync_reporter_timeout_sec;
-extern int  g_sync_weight_retry_max;
-extern int  g_sync_weight_retry_interval_sec;
-extern int  g_sync_tb_num_dedup_sec;
-extern int  g_sync_weight_snapshot_max_age_sec;  // fix24 v43 Bug#18
-
-// ==================== 内部状态 ====================
-
-// tb_num → 重量结果(promise/future 模式)
-struct PendingWeightEntry {
-    std::shared_ptr<std::promise<WeightDispatchResult>> prom;
-    std::shared_future<WeightDispatchResult>            fut;
-    std::atomic<bool>                                   done{false};
-};
-
-static std::unordered_map<std::string, std::shared_ptr<PendingWeightEntry>> g_weight_results;
-static std::mutex g_weight_results_mtx;
-
-// 进行中任务计数
-static std::atomic<int> g_pending_count{0};
-
-// tb_num 去重 map
-static std::unordered_map<std::string, time_t> g_tb_num_dedup_map;
-static std::mutex g_tb_num_dedup_mtx;
-
-// 秤访问串行化锁(物理秤只有一个,多 worker 并发读会导致共享状态竞态)
-// 覆盖:weight_scale_read() + db_insert_weight_record(),保证每次读秤完整隔离
-static std::mutex g_scale_access_mtx;
-
-// ==================== 内部辅助函数 ====================
-
-/**
- * 重量任务线程入口函数
- */
-static void weight_worker_func(const std::string& plate,
-                                const std::string& tb_num,
-                                int station_type,
-                                std::shared_ptr<PendingWeightEntry> entry) {
-    // RAII 清理守卫:无论正常/异常退出,均保证 pending 计数递减 + 通知等待者
-    struct CleanupGuard {
-        std::shared_ptr<PendingWeightEntry> entry;
-        WeightDispatchResult result;
-        bool committed = false;
-        ~CleanupGuard() {
-            if (!committed) {
-                // 异常路径:设置失败结果,防止等待者永久挂起
-                result.read_status = 2;
-                result.upload_status = 2;  // Bug#17: 终态失败,不留"待上传"
-                result.response_msg = "worker异常退出";
-                try { entry->prom->set_value(result); } catch (...) {}
-                entry->done.store(true);
-            }
-            g_pending_count.fetch_sub(1);
-        }
-    } guard{entry, {}, false};
-
-    WeightDispatchResult result;
-    result.plate = plate;
-    result.tb_num = tb_num;
-    result.station_type = station_type;
-
-    std::string in_out_str = (station_type == 1) ? "进站" : "出站";
-    std::cout << "[v43-重量] 开始任务: " << plate << " " << in_out_str
-              << " 联单:" << tb_num << std::endl;
-
-    try {
-        // ===== 步骤1-3:读秤 + 上传 + 写DB(串行化,保护物理秤共享状态)=====
-        {
-            std::lock_guard<std::mutex> scale_lock(g_scale_access_mtx);
-
-            // ===== 步骤1:读取重量 =====
-            if (!g_weight_enabled) {
-                std::cout << "[v43-重量] 称重未启用(flagWeight=0),跳过读取" << std::endl;
-                result.read_status = 2;
-                result.response_msg = "称重未启用";
-                result.upload_status = 2;  // Bug#17: 读秤失败/跳过为终态,标记失败而非"待上传"
-            } else {
-                double threshold_kg = (station_type == 1) ? g_weight_threshold_in : g_weight_threshold_out;
-                std::cout << "[v43-重量] 开始读取, 阈值:" << threshold_kg << "公斤" << std::endl;
-
-                double weight_kg = 0.0;
-                bool read_ok = false;
-
-                // fix24 v44 P0: 三层兜底读取
-                // ① 预检测锁定(最快,0等待)——TCP线程已提前锁定稳定重量
-                if (!read_ok) {
-                    read_ok = weight_scale_read_prelocked(weight_kg, threshold_kg);
-                }
-                // ② 快照优先(Bug#18,中等)——复用采样池近期稳定读数
-                if (!read_ok) {
-                    read_ok = weight_scale_read_snapshot(
-                        weight_kg, threshold_kg, g_sync_weight_snapshot_max_age_sec);
-                }
-                // ③ 阻塞等待(兜底)——已在 weight_scale_read_snapshot 内部实现
-
-                if (read_ok && weight_kg > 0) {
-                    result.weight_kg = weight_kg;
-                    result.read_status = 1;
-                    std::cout << "[v43-重量] 读取成功: " << weight_kg << "公斤" << std::endl;
-
-                    // ===== 步骤2:上传重量到平台(带重试) =====
-                    double weight_ton = weight_kg / 1000.0;
-                    bool upload_ok = false;
-                    
-                    // 调用 weight_scale.cpp 中的公开上传接口(带内部重试)
-                    int ret = upload_weight_with_retry_public(tb_num, static_cast<float>(weight_ton));
-                    upload_ok = (ret == 0);
-
-                    result.upload_status = upload_ok ? 1 : 2;
-                    result.response_msg = upload_ok ? "上传成功" : "上传失败(已重试)";
-                    std::cout << "[v43-重量] " << (upload_ok ? "✅上传成功" : "❌上传失败")
-                              << " 联单:" << tb_num << " 重量:" << weight_ton << "吨" << std::endl;
-                } else {
-                    result.read_status = 2;
-                    result.response_msg = "未获取有效重量";
-                    // fix24 v43 Bug#17: 读秤失败是终态(无重试路径会自动捞取),
-                    // upload_status 必须写 2(失败),写 0 会在页面永久显示"待上传"误导
-                    result.upload_status = 2;
-                    std::cout << "[v43-重量] ❌未读取到有效重量 联单:" << tb_num << std::endl;
-                }
-            }
-
-            // ===== 步骤3:记录到 weight_records 表 =====
-            db_insert_weight_record(plate, tb_num, station_type,
-                                    result.weight_kg, result.upload_status, result.response_msg);
-        }  // g_scale_access_mtx 在此释放,下一个 worker 可以读秤
-
-        // ===== 步骤4:通知等待者(Reporter / upload_thread) =====
-        entry->prom->set_value(result);
-        entry->done.store(true);
-        guard.result = result;
-        guard.committed = true;  // 正常路径已提交,析构只减计数
-
-        std::cout << "[v43-重量] 任务结束: " << plate << " 联单:" << tb_num
-                  << " 读取:" << (result.read_status == 1 ? "✅" : "❌")
-                  << " 上传:" << (result.upload_status == 1 ? "✅" : "❌") << std::endl;
-
-        // ===== 步骤5:MQTT发布(fix27 Bug#30) =====
-        if (g_mqtt_enabled) {
-            time_t now_t = time(nullptr);
-            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"
-                "车牌号:    " + plate + "\n"
-                "时间:       " + std::string(time_buf) + "\n"
-                "工地编号:  " + point_number + "\n"
-                "门号:        " + throughway + "\n"
-                "进出站:     " + in_out_str + "\n"
-                "车牌颜色:  " + g_plate_color + "\n"
-                "车型:         " + g_vehicle_type + "\n"
-                "三联单编号:" + tb_num + "\n"
-                "一组照片(1张低位照片,1张高位照片)上传成功";
-
-            if (g_weight_enabled) {
-                if (result.read_status == 1 && result.weight_kg > 0) {
-                    double weight_ton = result.weight_kg / 1000.0;
-                    msg += "\n称重数据: " + std::to_string(weight_ton) + "吨 ("
-                         + std::to_string(static_cast<int>(result.weight_kg)) + "公斤) ["
-                         + (result.upload_status == 1 ? "上传成功" : "上传失败") + "]";
-                } 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 << "[v43-MQTT] 发布成功: " << plate << " " << in_out_str << std::endl;
-                } else if (pub_att < 2) {
-                    std::cerr << "[v43-MQTT] 发布失败,重试(" << (pub_att+1) << "/3)" << std::endl;
-                    std::this_thread::sleep_for(std::chrono::seconds(1));
-                }
-            }
-            if (!mqtt_ok) {
-                std::cerr << "[v43-MQTT] 发布最终失败: " << plate << std::endl;
-            }
-        }
-    } catch (const std::exception& e) {
-        std::cerr << "[v43-重量] ❌worker异常: " << e.what()
-                  << " 联单:" << tb_num << std::endl;
-        // CleanupGuard 析构自动处理:设置失败结果 + 减计数
-    } catch (...) {
-        std::cerr << "[v43-重量] ❌worker未知异常 联单:" << tb_num << std::endl;
-        // CleanupGuard 析构自动处理
-    }
-}
-
-// ==================== 公开接口实现 ====================
-
-void weight_dispatcher_init() {
-    std::cout << "[v43-调度器] 初始化 (enabled=" << (g_sync_parallel_enabled ? 1 : 0)
-              << " timeout=" << g_sync_reporter_timeout_sec << "s"
-              << " retry_max=" << g_sync_weight_retry_max
-              << " dedup=" << g_sync_tb_num_dedup_sec << "s)" << std::endl;
-}
-
-void weight_dispatcher_shutdown() {
-    // 等待所有进行中的任务完成(最多等 60s)
-    int waited = 0;
-    while (g_pending_count.load() > 0 && waited < 60) {
-        std::this_thread::sleep_for(std::chrono::seconds(1));
-        waited++;
-    }
-    if (g_pending_count.load() > 0) {
-        std::cerr << "[v43-调度器] 关闭警告: 仍有 " << g_pending_count.load()
-                  << " 个任务未完成" << std::endl;
-    } else {
-        std::cout << "[v43-调度器] 已关闭(所有任务已完成)" << std::endl;
-    }
-}
-
-void weight_dispatch_async(const std::string& plate,
-                           const std::string& tb_num,
-                           int station_type) {
-    if (!g_sync_parallel_enabled) {
-        std::cerr << "[v43-调度器] 并行模式未启用,忽略 dispatch" << std::endl;
-        return;
-    }
-
-    // tb_num 去重检查
-    if (weight_dispatch_is_tb_num_dup(tb_num)) {
-        std::cout << "[v43-调度器] tb_num=" << tb_num << " 已处理过,跳过" << std::endl;
-        return;
-    }
-
-    // 创建 promise/future
-    auto entry = std::make_shared<PendingWeightEntry>();
-    entry->prom = std::make_shared<std::promise<WeightDispatchResult>>();
-    entry->fut = entry->prom->get_future().share();
-
-    {
-        std::lock_guard<std::mutex> lk(g_weight_results_mtx);
-        g_weight_results[tb_num] = entry;
-    }
-
-    g_pending_count.fetch_add(1);
-
-    // 启动独立重量线程(detach,通过 future 传递结果)
-    std::thread(weight_worker_func, plate, tb_num, station_type, entry).detach();
-
-    std::cout << "[v43-调度器] 已派发重量任务: " << plate << " 联单:" << tb_num
-              << " 待处理:" << g_pending_count.load() << std::endl;
-}
-
-bool weight_dispatch_wait_result(const std::string& tb_num,
-                                 int timeout_sec,
-                                 WeightDispatchResult& result) {
-    std::shared_future<WeightDispatchResult> fut;
-    {
-        std::lock_guard<std::mutex> lk(g_weight_results_mtx);
-        auto it = g_weight_results.find(tb_num);
-        if (it == g_weight_results.end()) {
-            return false;  // 未找到对应任务
-        }
-        fut = it->second->fut;
-    }
-
-    // 等待结果
-    auto status = fut.wait_for(std::chrono::seconds(timeout_sec));
-    if (status == std::future_status::ready) {
-        result = fut.get();
-        // fix27 Bug#29: 不清理 entry,支持多次调用(飞书消息+Reporter 都需要获取结果)
-        // entry 由 weight_dispatch_cleanup() 统一清理,防止内存泄漏
-        return true;
-    }
-    // 超时:也不清理,让 worker 后续完成时仍可被获取
-    // 最终由 weight_dispatch_cleanup() 在照片上传失败/超时后清理
-    return false;
-}
-
-bool weight_dispatch_is_ready(const std::string& tb_num) {
-    std::lock_guard<std::mutex> lk(g_weight_results_mtx);
-    auto it = g_weight_results.find(tb_num);
-    if (it == g_weight_results.end()) return false;
-    return it->second->done.load();
-}
-
-int weight_dispatch_pending_count() {
-    return g_pending_count.load();
-}
-
-void weight_dispatch_cleanup(const std::string& tb_num) {
-    std::lock_guard<std::mutex> lk(g_weight_results_mtx);
-    auto it = g_weight_results.find(tb_num);
-    if (it != g_weight_results.end()) {
-        std::cout << "[v43-调度器] 清理未消费 entry: " << tb_num << std::endl;
-        g_weight_results.erase(it);
-    }
-}
-
-bool weight_dispatch_is_tb_num_dup(const std::string& tb_num) {
-    std::lock_guard<std::mutex> lk(g_tb_num_dedup_mtx);
-    auto it = g_tb_num_dedup_map.find(tb_num);
-    if (it != g_tb_num_dedup_map.end()) {
-        time_t elapsed = time(nullptr) - it->second;
-        if (elapsed < g_sync_tb_num_dedup_sec) {
-            return true;  // 在去重窗口内
-        }
-    }
-    g_tb_num_dedup_map[tb_num] = time(nullptr);
-
-    // 清理过期条目(防止内存泄漏)
-    time_t now = time(nullptr);
-    for (auto jt = g_tb_num_dedup_map.begin(); jt != g_tb_num_dedup_map.end(); ) {
-        if (now - jt->second > g_sync_tb_num_dedup_sec * 2) {
-            jt = g_tb_num_dedup_map.erase(jt);
-        } else {
-            ++jt;
-        }
-    }
-    return false;
-}

+ 0 - 95
src/weight_dispatcher.h

@@ -1,95 +0,0 @@
-/**
- * weight_dispatcher.h — v43 并行称重调度器
- * 
- * fix24 v43 核心改造:
- * tb_num 确定后立即启动独立重量线程,与照片上传并行执行。
- * 重量读取不再依赖照片上传成功,两路通过 tb_num 原子关联。
- * 
- * 设计要点:
- * 1. dispatch() 在 save_file 后立即调用(main.cpp)
- * 2. 独立线程执行重量读取 + 平台上传 + DB 写入
- * 3. MQTT 去重 key 从 plate 改为 tb_num
- * 4. [sync] enabled=0 时完全不使用此模块,退回 v41 路径
- */
-#ifndef WEIGHT_DISPATCHER_H
-#define WEIGHT_DISPATCHER_H
-
-#include "common.h"
-#include <string>
-#include <atomic>
-
-// ==================== 重量任务结果 ====================
-struct WeightDispatchResult {
-    std::string plate;
-    std::string tb_num;
-    int         station_type = 0;     // 1=进站 2=出站
-    double      weight_kg    = 0.0;
-    int         read_status  = 0;     // 1=读取成功 2=读取失败/超时
-    int         upload_status = 0;    // 0=未上传 1=上传成功 2=上传失败
-    std::string response_msg;
-
-    bool success() const { return read_status == 1 && upload_status == 1; }
-};
-
-// ==================== 公开接口 ====================
-
-/**
- * 启动重量调度器(主进程初始化时调用一次)
- */
-void weight_dispatcher_init();
-
-/**
- * 关闭重量调度器(等待所有进行中的任务完成)
- */
-void weight_dispatcher_shutdown();
-
-/**
- * tb_num 确定后立即调用,启动独立重量线程。
- * 非阻塞,立即返回。
- *
- * @param plate        车牌号
- * @param tb_num       联单编号(核心关联 key)
- * @param station_type 1=进站 2=出站
- */
-void weight_dispatch_async(const std::string& plate,
-                           const std::string& tb_num,
-                           int station_type);
-
-/**
- * 按 tb_num 查询重量结果(供 Reporter / upload_thread 等待)。
- * 阻塞直到结果可用或超时。
- *
- * @param tb_num        联单编号
- * @param timeout_sec   最大等待秒数(默认 30s)
- * @param[out] result   结果输出
- * @return true=结果已填充, false=超时未等到
- */
-bool weight_dispatch_wait_result(const std::string& tb_num,
-                                 int timeout_sec,
-                                 WeightDispatchResult& result);
-
-/**
- * 检查指定 tb_num 的重量任务是否已完成(非阻塞)
- */
-bool weight_dispatch_is_ready(const std::string& tb_num);
-
-/**
- * 获取当前进行中的重量任务数量(用于诊断)
- */
-int  weight_dispatch_pending_count();
-
-/**
- * 清理指定 tb_num 的 map entry(防止照片上传失败时 entry 泄漏)。
- * 如果 entry 不存在或已被消费,则无操作。
- */
-void weight_dispatch_cleanup(const std::string& tb_num);
-
-// ==================== tb_num 去重(替代旧的 plate 去重) ====================
-
-/**
- * 检查 tb_num 是否在去重窗口内已处理过。
- * 如果未处理,记录并返回 false;如果已处理,返回 true。
- */
-bool weight_dispatch_is_tb_num_dup(const std::string& tb_num);
-
-#endif

+ 10 - 209
src/weight_scale.cpp

@@ -21,26 +21,13 @@ static const int RECONNECT_DELAY_MS = 5000;
 static const int READ_INTERVAL_MS = 200;
 
 // ==================== TCP连接状态 ====================
-// fix24 v43 Bug#18: 稳定样本带采集时间戳,支持"快照优先"读取
-// (出站空车过秤快,任务线程启动后清空采样池可能等不到稳定读数,
-//   但任务启动前 TCP 采集线程往往已收到该车的稳定样本,需能复用)
-struct WeightSample {
-    double weight_kg;
-    time_t ts;
-};
 static std::vector<uint8_t> g_recv_buffer;
-static std::vector<WeightSample> g_stability_samples;
+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;
 
-// ==================== fix24 v44 P0: 预检测锁定状态 ====================
-// TCP线程每200ms采样,连续N个样本稳定且 > 阈值 → 原子锁定重量值
-// 车牌检测后 weight_scale_read_prelocked() 优先消费(0等待)
-static std::atomic<bool>   g_weight_prelocked{false};
-static std::atomic<double> g_prelocked_weight_kg{0.0};
-
 // ==================== 称重+MQTT异步任务队列 ====================
 struct WeightMqttTask {
     std::string plate;
@@ -98,55 +85,15 @@ static void process_buffer() {
         if (parse_frame(frame, weight_kg, status)) {
             // ✅ fix21b: 与plate_log2mqtt原始逻辑一致:排除status='G'/'H'的帧,接受其他状态
             // 耀华仪表的'G'/'H'状态码在此仪表上不代表有效称重数据
-
-            // fix24 v44 P0+Bug#25: 预检测——重量低于阈值30%时清除锁定(车已下秤)
-            // 此检查在锁外执行(原子操作),仅在 status 有效时执行(避免用无效数据误清除)
-            if (g_weight_prelock_threshold_kg > 0 && status != 'G' && status != 'H'
-                && weight_kg < g_weight_prelock_threshold_kg * 0.3) {
-                if (g_weight_prelocked.load(std::memory_order_acquire)) {
-                    g_weight_prelocked.store(false, std::memory_order_release);
-                    g_prelocked_weight_kg.store(0.0, std::memory_order_release);
-                    std::cout << "[称重-v44] 预锁定清除(重量降至" << weight_kg << "kg,车已下秤)" << std::endl;
-                }
-            }
-
             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, time(nullptr)});
+                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);
                 }
-
-                // fix24 v44 P0: 预检测锁定检查(在锁内执行,TCP线程独占)
-                // 地磅场景:车辆整体上秤,连续N个样本稳定且 > 阈值 → 锁定
-                if (g_weight_prelock_threshold_kg > 0) {
-                    int n = g_weight_prelock_stable_samples;
-                    if ((int)g_stability_samples.size() >= n) {
-                        int start_idx = (int)g_stability_samples.size() - n;
-                        double min_w = g_stability_samples[start_idx].weight_kg;
-                        double max_w = min_w;
-                        double sum_w = 0;
-                        for (int i = start_idx; i < (int)g_stability_samples.size(); i++) {
-                            min_w = std::min(min_w, g_stability_samples[i].weight_kg);
-                            max_w = std::max(max_w, g_stability_samples[i].weight_kg);
-                            sum_w += g_stability_samples[i].weight_kg;
-                        }
-                        double avg_w = sum_w / n;
-                        // 稳定且超过预检测阈值 → 锁定(仅从未锁定→锁定时触发)
-                        if (avg_w > g_weight_prelock_threshold_kg
-                            && (max_w - min_w) <= g_weight_stable_threshold
-                            && !g_weight_prelocked.load(std::memory_order_acquire)) {
-                            g_prelocked_weight_kg.store(avg_w, std::memory_order_release);
-                            g_weight_prelocked.store(true, std::memory_order_release);
-                            std::cout << "[称重-v44] 预锁定: " << avg_w << "公斤"
-                                      << " (波动" << (max_w - min_w) << "kg"
-                                      << " 阈值" << g_weight_prelock_threshold_kg << "kg)" << std::endl;
-                        }
-                    }
-                }
             }
         }
         g_recv_buffer.erase(g_recv_buffer.begin(), g_recv_buffer.begin() + frame_start + FRAME_LENGTH);
@@ -183,12 +130,6 @@ static void tcp_client_loop() {
         g_recv_buffer.clear();
         g_candidate_weights.clear();
         g_stability_samples.clear();
-        // fix24 v44 Bug#24: 重连后清除预检测锁定(旧数据已过期)
-        if (g_weight_prelocked.load(std::memory_order_acquire)) {
-            g_weight_prelocked.store(false, std::memory_order_release);
-            g_prelocked_weight_kg.store(0.0, std::memory_order_release);
-            std::cout << "[称重-v44] TCP重连,预锁定已清除" << std::endl;
-        }
 
         while (g_weight_running.load()) {
             uint8_t buf[BUFFER_SIZE];
@@ -209,48 +150,11 @@ static void tcp_client_loop() {
 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 = g_stability_samples.front().weight_kg;
-    double max_w = min_w;
-    for (const auto& s : g_stability_samples) {
-        min_w = std::min(min_w, s.weight_kg);
-        max_w = std::max(max_w, s.weight_kg);
-    }
+    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;
 }
 
-/**
- * fix24 v43 Bug#18: 快照模式检查——采样池中近 max_age_sec 秒内
- * 是否已有一组稳定读数。不修改采样池(与 is_weight_stable 的区别:
- * 增加时间新鲜度校验,防止复用滑窗内上一辆车的残留样本)。
- * 调用方需已持有 g_data_mutex。
- */
-static bool is_recent_stable_locked(double& out_weight_kg, double threshold_kg, int max_age_sec) {
-    if ((int)g_stability_samples.size() < g_weight_stable_samples) return false;
-    time_t now = time(nullptr);
-    double min_w = 0.0, max_w = 0.0, sum_w = 0.0;
-    int fresh_cnt = 0;
-    for (const auto& s : g_stability_samples) {
-        if (now - s.ts <= max_age_sec) {
-            if (fresh_cnt == 0) { min_w = max_w = s.weight_kg; }
-            else {
-                min_w = std::min(min_w, s.weight_kg);
-                max_w = std::max(max_w, s.weight_kg);
-            }
-            ++fresh_cnt;
-            sum_w += s.weight_kg;
-        }
-    }
-    // 要求窗口内新鲜样本数达到稳定样本数要求且波动达标
-    if (fresh_cnt < g_weight_stable_samples) return false;
-    if ((max_w - min_w) > g_weight_stable_threshold) return false;
-    double avg_w = sum_w / fresh_cnt;
-    if (avg_w > threshold_kg && avg_w >= g_weight_min_kg) {
-        out_weight_kg = avg_w;
-        return true;
-    }
-    return false;
-}
-
 static double get_max_candidate_weight() {
     std::lock_guard<std::mutex> lock(g_data_mutex);
     if (g_candidate_weights.empty()) return 0.0;
@@ -280,7 +184,7 @@ static bool read_weight_with_timeout(double threshold_kg, double& out_weight_ton
             {
                 std::lock_guard<std::mutex> lock(g_data_mutex);
                 if (!g_stability_samples.empty())
-                    latest_w = g_stability_samples.back().weight_kg;
+                    latest_w = g_stability_samples.back();
             }
             std::cout << "[称重] 等待中... " << wait_seconds << "/" << g_weight_detection_time << "秒"
                       << " 候选:" << cur_candidates << "/" << g_weight_candidate_count
@@ -311,12 +215,8 @@ static bool read_weight_with_timeout(double threshold_kg, double& out_weight_ton
                   << " | 候选数:" << g_candidate_weights.size() << "/" << g_weight_candidate_count
                   << " 稳定样本:" << g_stability_samples.size() << "/" << g_weight_stable_samples;
         if (!g_stability_samples.empty()) {
-            double min_w = g_stability_samples.front().weight_kg;
-            double max_w = min_w;
-            for (const auto& s : g_stability_samples) {
-                min_w = std::min(min_w, s.weight_kg);
-                max_w = std::max(max_w, s.weight_kg);
-            }
+            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 {
@@ -431,31 +331,13 @@ bool weight_scale_init() {
         std::cerr << "[称重] 稳定阈值过低(" << g_weight_stable_threshold << "公斤),修正为50" << std::endl;
         g_weight_stable_threshold = 50.0;
     }
-    // fix24 v44 Bug#23: 预检测样本数不能超过采样池容量
-    if (g_weight_prelock_threshold_kg > 0 && g_weight_prelock_stable_samples > g_weight_stable_samples) {
-        std::cerr << "[称重-v44] 预检测样本数(" << g_weight_prelock_stable_samples
-                  << ")超过采样池容量(" << g_weight_stable_samples
-                  << "),修正为" << g_weight_stable_samples << std::endl;
-        g_weight_prelock_stable_samples = g_weight_stable_samples;
-    }
-    // fix24 v44 Bug#26: 预检测阈值必须 >= 最小有效重量,否则锁定值无法被消费
-    if (g_weight_prelock_threshold_kg > 0 && g_weight_prelock_threshold_kg < g_weight_min_kg) {
-        std::cerr << "[称重-v44] ⚠️预检测阈值(" << g_weight_prelock_threshold_kg
-                  << "kg) < 最小有效重量(" << g_weight_min_kg
-                  << "kg),锁定值无法被消费,修正为" << g_weight_min_kg << "kg" << std::endl;
-        g_weight_prelock_threshold_kg = g_weight_min_kg;
-    }
 
     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
-              << " 预检测阈值:" << g_weight_prelock_threshold_kg << "公斤"
-              << (g_weight_prelock_threshold_kg > 0 ?
-                  ("(需" + std::to_string(g_weight_prelock_stable_samples) + "个稳定样本)") :
-                  "(已禁用)") << std::endl;
+              << " 候选数量:" << g_weight_candidate_count << std::endl;
 
     g_weight_running = true;
     g_tcp_thread = new std::thread(tcp_client_loop);
@@ -474,63 +356,6 @@ bool weight_scale_read(double& weight_kg, int timeout_sec) {
     return false;
 }
 
-// fix24 v43 Bug#18: 快照优先读取
-// ① 先看采样池近 max_age_sec 秒是否已有稳定读数(车辆在秤时 TCP 线程持续收数,
-//    识别+建单完成时往往已稳定),命中直接返回,且不浪费已采集样本;
-// ② 未命中再清空采样池走原有阻塞等待(进站重车/上秤慢的场景不受影响)。
-bool weight_scale_read_snapshot(double& weight_kg, double threshold_kg, int max_age_sec) {
-    // ===== ① 快照检查 =====
-    {
-        std::lock_guard<std::mutex> lock(g_data_mutex);
-        double snap_kg = 0.0;
-        if (is_recent_stable_locked(snap_kg, threshold_kg, max_age_sec)) {
-            weight_kg = snap_kg;
-            std::cout << "[称重] 快照命中(近" << max_age_sec << "秒内已稳定): "
-                      << snap_kg << "公斤 (" << (snap_kg / 1000.0) << "吨)" << std::endl;
-            // 消费掉快照,避免后续任务复用本次样本
-            g_candidate_weights.clear();
-            g_stability_samples.clear();
-            return true;
-        }
-    }
-
-    // ===== ② 快照未命中:清空后阻塞等待稳定(原逻辑) =====
-    clear_candidate_data();
-    double weight_ton = 0.0;
-    if (read_weight_with_timeout(threshold_kg, weight_ton)) {
-        weight_kg = weight_ton * 1000.0;
-        return true;
-    }
-    return false;
-}
-
-// fix24 v44 P0: 预检测锁定读取(三层兜底的第①层)
-// TCP线程已在后台检测到重量稳定并锁定 → 直接消费,0等待
-// 适用于地磅场景:车辆整体上秤,重量一次性加载,TCP线程在车牌检测前就已完成锁定
-bool weight_scale_read_prelocked(double& weight_kg, double threshold_kg) {
-    if (g_weight_prelock_threshold_kg <= 0) return false;  // 预检测未启用
-    if (!g_weight_prelocked.load(std::memory_order_acquire)) return false;
-
-    double w = g_prelocked_weight_kg.load(std::memory_order_acquire);
-    if (w <= threshold_kg || w < g_weight_min_kg) return false;
-
-    // 命中:消费锁定值(一次性读取)
-    weight_kg = w;
-    g_weight_prelocked.store(false, std::memory_order_release);
-    g_prelocked_weight_kg.store(0.0, std::memory_order_release);
-
-    // 同步清空采样池,防止后续任务复用本次车辆的样本
-    {
-        std::lock_guard<std::mutex> lock(g_data_mutex);
-        g_stability_samples.clear();
-        g_candidate_weights.clear();
-    }
-
-    std::cout << "[称重-v44] ✅预锁定命中: " << weight_kg << "公斤 ("
-              << (weight_kg / 1000.0) << "吨) — 0等待" << std::endl;
-    return true;
-}
-
 void weight_scale_close() {
     g_weight_running = false;
     if (g_tcp_thread) {
@@ -607,22 +432,8 @@ static void weight_mqtt_worker_func() {
                 g_weight_threshold_in : g_weight_threshold_out;
             std::cout << "[称重] 开始读取, 阈值:" << threshold_kg << "公斤" << std::endl;
 
-            // fix24 v44 P0: 三层兜底读取(v41/v43 两条路径统一)
-            // ① 预检测锁定(最快,0等待)
-            double read_kg = 0.0;
-            if (weight_scale_read_prelocked(read_kg, threshold_kg)) {
-                weight_read_ok = true;
-                weight_ton = read_kg / 1000.0;
-            }
-            // ② 快照优先(Bug#18)
-            if (!weight_read_ok) {
-                double snap_kg = 0.0;
-                if (weight_scale_read_snapshot(snap_kg, threshold_kg, g_sync_weight_snapshot_max_age_sec)) {
-                    weight_read_ok = true;
-                    weight_ton = snap_kg / 1000.0;
-                }
-            }
-            // ③ 阻塞等待(已在 weight_scale_read_snapshot 内部实现)
+            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;
@@ -771,13 +582,3 @@ bool weight_manual_retry(int record_id) {
         return false;
     }
 }
-
-// ==================== fix24 v43: 公开接口(供 weight_dispatcher 调用) ====================
-
-int upload_weight_data_public(const std::string& tb_no, float weight_ton) {
-    return upload_weight_data(tb_no, weight_ton);
-}
-
-int upload_weight_with_retry_public(const std::string& tb_no, float weight_ton) {
-    return upload_weight_with_retry(tb_no, weight_ton);
-}

+ 0 - 40
src/weight_scale.h

@@ -22,32 +22,6 @@ bool weight_scale_init();
  */
 bool weight_scale_read(double& weight_kg, int timeout_sec);
 
-/**
- * fix24 v43 Bug#18: 快照优先读取称重数据
- * 先检查采样池中近 max_age_sec 秒内是否已有稳定读数(车辆上秤后
- * TCP采集线程持续收数,识别+建单完成时往往已稳定),命中则直接返回;
- * 未命中则清空采样池并阻塞等待 g_weight_detection_time 秒。
- * 解决出站空车过秤快、清空后等不到稳定读数导致"未获取有效重量"的竞态。
- *
- * @param weight_kg   输出:重量(kg)
- * @param threshold_kg 最小有效重量阈值(kg),低于此值视为无效
- * @param max_age_sec 快照最大年龄(秒),仅接受此时长内采集的稳定样本
- * @return true=读取成功, false=超时或失败
- */
-bool weight_scale_read_snapshot(double& weight_kg, double threshold_kg, int max_age_sec);
-
-/**
- * fix24 v44 P0: 预检测锁定读取(最快路径,0等待)
- * 检查 TCP 线程是否已提前锁定稳定重量(车辆上秤后重量 > PRELOCK_THRESHOLD_KG
- * 且连续稳定),命中则直接返回并消费锁定值。
- * 仅适用于地磅场景(整车一次性上秤,无半上秤误判)。
- *
- * @param weight_kg    输出:重量(kg)
- * @param threshold_kg 最小有效重量阈值(kg),低于此值视为无效
- * @return true=预锁定命中, false=无锁定值(需走快照/阻塞等待)
- */
-bool weight_scale_read_prelocked(double& weight_kg, double threshold_kg);
-
 /**
  * 关闭称重系统(停止TCP线程)
  */
@@ -79,18 +53,4 @@ void stop_weight_mqtt_worker();
  */
 bool weight_manual_retry(int record_id);
 
-// ==================== fix24 v43: 并行称重调度器接口 ====================
-/**
- * 重量数据上传到平台(公开接口,供 weight_dispatcher 调用)
- * @param tb_no 联单编号
- * @param weight_ton 重量(吨)
- * @return 0=成功, -1=失败
- */
-int upload_weight_data_public(const std::string& tb_no, float weight_ton);
-
-/**
- * 带重试的重量上传(公开接口)
- */
-int upload_weight_with_retry_public(const std::string& tb_no, float weight_ton);
-
 #endif