commit cc6e66bbf8e82dd08c64787e6fb79ab14ec054b8
Author: ninghongbin <2409766686@qq.com>
Date: Tue Sep 30 17:17:20 2025 +0800
内容安全审核
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/Detect.iml b/.idea/Detect.iml
new file mode 100644
index 0000000..d870a4a
--- /dev/null
+++ b/.idea/Detect.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..9f642d0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..3c627ef
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..20b9235
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__pycache__/main.cpython-310.pyc b/__pycache__/main.cpython-310.pyc
new file mode 100644
index 0000000..013daf8
Binary files /dev/null and b/__pycache__/main.cpython-310.pyc differ
diff --git a/config.ini b/config.ini
new file mode 100644
index 0000000..000a996
--- /dev/null
+++ b/config.ini
@@ -0,0 +1,20 @@
+[server]
+port = 8000
+
+[mysql]
+host = 192.168.110.65
+port = 6975
+user = video_check
+password = fsjPfhxCs8NrFGmL
+database = video_check
+charset = utf8mb4
+
+[jwt]
+secret_key = 6tsieyd87wefdw2wgeduwte23rfcsd
+algorithm = HS256
+access_token_expire_minutes = 30
+
+[business]
+ocr_conf = 0.6
+face_conf = 0.6
+yolo_conf = 0.7
\ No newline at end of file
diff --git a/core/__pycache__/detect.cpython-310.pyc b/core/__pycache__/detect.cpython-310.pyc
new file mode 100644
index 0000000..8565bc0
Binary files /dev/null and b/core/__pycache__/detect.cpython-310.pyc differ
diff --git a/core/detect.py b/core/detect.py
new file mode 100644
index 0000000..4bc51a0
--- /dev/null
+++ b/core/detect.py
@@ -0,0 +1,140 @@
+import json
+import re
+
+from mysql.connector import Error as MySQLError
+
+from ds.config import BUSINESS_CONFIG
+from ds.db import db
+from service.face_service import detect as faceDetect,init_insightface
+from service.model_service import load_yolo_model,detect as yoloDetect
+from service.ocr_service import detect as ocrDetect,init_ocr_engine
+from service.file_service import save_detect_file, save_detect_yolo_file, save_detect_face_file
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+
+
+# 创建线程池执行器
+executor = ThreadPoolExecutor(max_workers=10)
+def init():
+ # # 人脸相关
+ init_insightface()
+ # # 初始化OCR引擎
+ init_ocr_engine()
+ #初始化YOLO模型
+ load_yolo_model()
+
+def save_db(model_type, client_ip, result):
+ conn = None
+ cursor = None
+ try:
+ # 连接数据库
+ conn = db.get_connection()
+ # 往表插入数据
+ cursor = conn.cursor(dictionary=True) # 返回字典格式结果
+ insert_query = """
+ INSERT INTO device_danger (client_ip, type, result)
+ VALUES (%s, %s, %s)
+ """
+ cursor.execute(insert_query, (client_ip, model_type, result))
+ conn.commit()
+ except MySQLError as e:
+ raise Exception(f"获取设备列表失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+def detectFrame(client_ip, frame):
+
+ # YOLO检测
+ yolo_flag, yolo_result = yoloDetect(frame, float(BUSINESS_CONFIG["yolo_conf"]))
+ if yolo_flag:
+ print(f"❌ 检测到违规内容,保存图片,YOLO")
+ danger_handler(client_ip)
+ path = save_detect_yolo_file(client_ip, frame, yolo_result, "yolo")
+ save_db(model_type="色情", client_ip=client_ip, result=str(path))
+
+ # 人脸检测
+ face_flag, face_result = faceDetect(frame, float(BUSINESS_CONFIG["face_conf"]))
+ if face_flag:
+ print(f"❌ 检测到违规内容,保存图片,FACE")
+ print("人脸识别内容:", face_result)
+ model_type = extract_face_names(face_result)
+ danger_handler(client_ip)
+ path = save_detect_face_file(client_ip, frame, face_result, "face")
+ save_db(model_type=model_type, client_ip=client_ip, result=str(path))
+
+ # OCR检测部分(使用修正后的提取函数)
+ ocr_flag, ocr_result = ocrDetect(frame, float(BUSINESS_CONFIG["ocr_conf"]))
+ if ocr_flag:
+ print(f"❌ 检测到违规内容,保存图片,OCR")
+ print("ocr识别内容:", ocr_result)
+ danger_handler(client_ip)
+ path = save_detect_file(client_ip, frame, "ocr")
+ save_db(model_type=str(ocr_result), client_ip=client_ip, result=str(path))
+
+ # 仅当所有检测均未发现违规时才提示
+ if not (face_flag or yolo_flag or ocr_flag):
+ print(f"所有模型未检测到任何违规内容")
+
+def danger_handler(client_ip):
+ from ws.ws import send_message_to_client, get_current_time_str
+ from service.device_service import increment_alarm_count_by_ip
+ from service.device_service import update_is_need_handler_by_client_ip
+
+ danger_msg = {
+ "type": "danger",
+ "timestamp": get_current_time_str(),
+ "client_ip": client_ip,
+ }
+ asyncio.run(
+ send_message_to_client(
+ client_ip=client_ip,
+ json_data=json.dumps(danger_msg)
+ )
+ )
+ lock_msg = {
+ "type": "lock",
+ "timestamp": get_current_time_str(),
+ "client_ip": client_ip
+ }
+ asyncio.run(
+ send_message_to_client(
+ client_ip=client_ip,
+ json_data=json.dumps(lock_msg)
+ )
+ )
+
+ # 增加危险记录次数
+ increment_alarm_count_by_ip(client_ip)
+
+ # 更新设备状态为未处理
+ update_is_need_handler_by_client_ip(client_ip, 1)
+
+def extract_prohibited_words(ocr_result: str) -> str:
+ """
+ 从多文本块的ocr_result中提取所有违禁词(去重后用逗号拼接)
+ 适配格式:多个"文本: ... 包含违禁词: ...;"片段
+ """
+ # 用正则匹配所有"包含违禁词: ...;"的片段(非贪婪匹配到分号)
+ # 匹配规则:"包含违禁词: "后面的内容,直到遇到";"结束
+ pattern = r"包含违禁词: (.*?);"
+ all_prohibited_segments = re.findall(pattern, ocr_result, re.DOTALL)
+
+ all_words = []
+ for segment in all_prohibited_segments:
+ # 去除每个片段中的置信度信息(如"(置信度: 1.00)")
+ cleaned = re.sub(r"\s*\([^)]*\)", "", segment.strip())
+ # 分割词语并过滤空值
+ words = [word.strip() for word in cleaned.split(",") if word.strip()]
+ all_words.extend(words)
+
+ # 去重后用逗号拼接
+ unique_words = list(set(all_words))
+ return ",".join(unique_words)
+
+
+def extract_face_names(face_result: str) -> str:
+ pattern = r"匹配: (.*?) \("
+ all_names = re.findall(pattern, face_result)
+ unique_names = list(set([name.strip() for name in all_names if name.strip()]))
+ return ",".join(unique_names)
diff --git a/ds/__pycache__/config.cpython-310.pyc b/ds/__pycache__/config.cpython-310.pyc
new file mode 100644
index 0000000..5444981
Binary files /dev/null and b/ds/__pycache__/config.cpython-310.pyc differ
diff --git a/ds/__pycache__/db.cpython-310.pyc b/ds/__pycache__/db.cpython-310.pyc
new file mode 100644
index 0000000..3ba379d
Binary files /dev/null and b/ds/__pycache__/db.cpython-310.pyc differ
diff --git a/ds/config.py b/ds/config.py
new file mode 100644
index 0000000..11fb735
--- /dev/null
+++ b/ds/config.py
@@ -0,0 +1,17 @@
+import configparser
+import os
+
+# 读取配置文件路径
+config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../config.ini")
+
+# 初始化配置解析器
+config = configparser.ConfigParser()
+
+# 读取配置文件
+config.read(config_path, encoding="utf-8")
+
+# 暴露配置项(方便其他文件调用)
+SERVER_CONFIG = config["server"]
+MYSQL_CONFIG = config["mysql"]
+JWT_CONFIG = config["jwt"]
+BUSINESS_CONFIG = config["business"]
diff --git a/ds/db.py b/ds/db.py
new file mode 100644
index 0000000..dff996d
--- /dev/null
+++ b/ds/db.py
@@ -0,0 +1,59 @@
+import mysql.connector
+from mysql.connector import Error
+from .config import MYSQL_CONFIG
+
+_connection_pool = None
+
+
+class Database:
+ """MySQL 连接池管理类"""
+ pool_config = {
+ "host": MYSQL_CONFIG.get("host", "localhost"),
+ "port": int(MYSQL_CONFIG.get("port", 3306)),
+ "user": MYSQL_CONFIG.get("user", "root"),
+ "password": MYSQL_CONFIG.get("password", ""),
+ "database": MYSQL_CONFIG.get("database", ""),
+ "charset": MYSQL_CONFIG.get("charset", "utf8mb4"),
+ "pool_name": "fastapi_pool",
+ "pool_size": 5,
+ "pool_reset_session": True
+ }
+
+ @classmethod
+ def get_connection(cls):
+ """获取数据库连接"""
+ try:
+ # 从连接池获取连接
+ conn = mysql.connector.connect(**cls.pool_config)
+ if conn.is_connected():
+ return conn
+ except Error as e:
+ raise Exception(f"MySQL 连接失败: {str(e)}") from e
+
+ @classmethod
+ def close_connection(cls, conn, cursor=None):
+ """关闭连接和游标"""
+ try:
+ if cursor:
+ cursor.close()
+ if conn and conn.is_connected():
+ conn.close()
+ except Error as e:
+ raise Exception(f"MySQL 连接关闭失败: {str(e)}") from e
+
+ @classmethod
+ def close_all_connections(cls):
+ """清理连接池(服务重启前调用)"""
+ try:
+ # 先检查属性是否存在、再判断是否有值
+ if hasattr(cls, "_connection_pool") and cls._connection_pool:
+ cls._connection_pool = None # 重置连接池
+ print("[Database] 连接池已重置、旧连接将被自动清理")
+ else:
+ print("[Database] 连接池未初始化或已重置、无需操作")
+ except Exception as e:
+ print(f"[Database] 重置连接池失败: {str(e)}")
+
+
+# 暴露数据库操作工具
+db = Database()
diff --git a/encryption/__pycache__/encrypt_decorator.cpython-310.pyc b/encryption/__pycache__/encrypt_decorator.cpython-310.pyc
new file mode 100644
index 0000000..3a8e426
Binary files /dev/null and b/encryption/__pycache__/encrypt_decorator.cpython-310.pyc differ
diff --git a/encryption/__pycache__/encryption.cpython-310.pyc b/encryption/__pycache__/encryption.cpython-310.pyc
new file mode 100644
index 0000000..171a3a1
Binary files /dev/null and b/encryption/__pycache__/encryption.cpython-310.pyc differ
diff --git a/encryption/encrypt_decorator.py b/encryption/encrypt_decorator.py
new file mode 100644
index 0000000..23e04f6
--- /dev/null
+++ b/encryption/encrypt_decorator.py
@@ -0,0 +1,40 @@
+import json
+from datetime import datetime
+from functools import wraps
+from typing import Any
+
+from encryption.encryption import aes_encrypt
+from schema.response_schema import APIResponse
+from pydantic import BaseModel
+
+
+def encrypt_response(field: str = "data"):
+ """接口返回值加密装饰器:正确序列化自定义对象为JSON"""
+
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(*args, **kwargs):
+ original_response: APIResponse = await func(*args, **kwargs)
+ field_value = getattr(original_response, field)
+
+ if not field_value:
+ return original_response
+
+ # 自定义JSON序列化函数:处理Pydantic模型和datetime
+ def json_default(obj: Any) -> Any:
+ if isinstance(obj, BaseModel):
+ return obj.model_dump()
+ if isinstance(obj, datetime):
+ return obj.isoformat()
+ return str(obj)
+
+ # 使用自定义序列化函数、确保生成标准JSON
+ field_value_json = json.dumps(field_value, default=json_default)
+ encrypted_data = aes_encrypt(field_value_json)
+ setattr(original_response, field, encrypted_data)
+
+ return original_response
+
+ return wrapper
+
+ return decorator
\ No newline at end of file
diff --git a/encryption/encryption.py b/encryption/encryption.py
new file mode 100644
index 0000000..00ef111
--- /dev/null
+++ b/encryption/encryption.py
@@ -0,0 +1,56 @@
+import os
+import base64
+from Crypto.Cipher import AES
+from Crypto.Util.Padding import pad, unpad
+from fastapi import HTTPException
+
+# 硬编码AES密钥(32字节、AES-256)
+AES_SECRET_KEY = b"jr1vA6tfWMHOYi6UXw67UuO6fdak2rMa"
+AES_BLOCK_SIZE = 16 # AES固定块大小
+
+# 校验密钥长度
+valid_key_lengths = [16, 24, 32]
+if len(AES_SECRET_KEY) not in valid_key_lengths:
+ raise ValueError(
+ f"AES密钥长度必须为{valid_key_lengths}字节、当前为{len(AES_SECRET_KEY)}字节"
+ )
+
+
+def aes_encrypt(plaintext: str) -> dict:
+ """AES-CBC模式加密(返回密文+IV、均为Base64编码)"""
+ try:
+ # 生成随机IV(16字节)
+ iv = os.urandom(AES_BLOCK_SIZE)
+
+ # 创建加密器
+ cipher = AES.new(AES_SECRET_KEY, AES.MODE_CBC, iv)
+
+ # 明文填充并加密
+ padded_plaintext = pad(plaintext.encode("utf-8"), AES_BLOCK_SIZE)
+ ciphertext = base64.b64encode(cipher.encrypt(padded_plaintext)).decode("utf-8")
+ iv_base64 = base64.b64encode(iv).decode("utf-8")
+
+ return {
+ "ciphertext": ciphertext,
+ "iv": iv_base64,
+ "algorithm": "AES-CBC"
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"AES加密失败:{str(e)}") from e
+
+
+def aes_decrypt(ciphertext: str, iv: str) -> str:
+ """AES-CBC模式解密"""
+ try:
+ # 解码Base64
+ ciphertext_bytes = base64.b64decode(ciphertext)
+ iv_bytes = base64.b64decode(iv)
+
+ # 创建解密器
+ cipher = AES.new(AES_SECRET_KEY, AES.MODE_CBC, iv_bytes)
+
+ # 解密并去填充
+ decrypted_bytes = unpad(cipher.decrypt(ciphertext_bytes), AES_BLOCK_SIZE)
+ return decrypted_bytes.decode("utf-8")
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"AES解密失败:{str(e)}") from e
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..cdcfba4
--- /dev/null
+++ b/main.py
@@ -0,0 +1,66 @@
+import uvicorn
+import os
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+# 原有业务导入
+from ds.config import SERVER_CONFIG
+from middle.error_handler import global_exception_handler
+from router.user_router import router as user_router
+from router.sensitive_router import router as sensitive_router
+from router.face_router import router as face_router
+from router.device_router import router as device_router
+from router.model_router import router as model_router
+from router.file_router import router as file_router
+from router.device_danger_router import router as device_danger_router
+from core.detect import init
+from ws.ws import ws_router, lifespan
+
+# 初始化 FastAPI 应用
+app = FastAPI(
+ title="内容安全审核后台",
+ description="含图片访问服务和动态模型管理",
+ version="1.0.0",
+ lifespan=lifespan
+)
+
+ALLOWED_ORIGINS = [
+ "*"
+]
+
+# 配置 CORS 中间件
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=ALLOWED_ORIGINS, # 允许的前端域名
+ allow_credentials=True, # 允许携带 Cookie
+ allow_methods=["*"], # 允许所有 HTTP 方法
+ allow_headers=["*"], # 允许所有请求头
+)
+
+# 注册路由
+app.include_router(user_router)
+app.include_router(device_router)
+app.include_router(face_router)
+app.include_router(sensitive_router)
+app.include_router(model_router)
+app.include_router(file_router)
+app.include_router(device_danger_router)
+app.include_router(ws_router)
+
+# 注册全局异常处理器
+app.add_exception_handler(Exception, global_exception_handler)
+
+# 主服务启动入口
+if __name__ == "__main__":
+ # 启动 FastAPI 主服务(仅使用8000端口)
+ port = int(SERVER_CONFIG.get("port", 8000))
+ # 加载所有模型
+ init()
+ uvicorn.run(
+ app="main:app",
+ host="0.0.0.0",
+ port=port,
+ workers=1,
+ ws="websockets",
+ reload=False
+ )
diff --git a/middle/__pycache__/auth_middleware.cpython-310.pyc b/middle/__pycache__/auth_middleware.cpython-310.pyc
new file mode 100644
index 0000000..401d969
Binary files /dev/null and b/middle/__pycache__/auth_middleware.cpython-310.pyc differ
diff --git a/middle/__pycache__/error_handler.cpython-310.pyc b/middle/__pycache__/error_handler.cpython-310.pyc
new file mode 100644
index 0000000..d268c3b
Binary files /dev/null and b/middle/__pycache__/error_handler.cpython-310.pyc differ
diff --git a/middle/auth_middleware.py b/middle/auth_middleware.py
new file mode 100644
index 0000000..b12cad1
--- /dev/null
+++ b/middle/auth_middleware.py
@@ -0,0 +1,102 @@
+from datetime import datetime, timedelta, timezone
+from typing import Optional
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from jose import JWTError, jwt
+from passlib.context import CryptContext
+
+from ds.config import JWT_CONFIG
+from ds.db import db
+
+# ------------------------------
+# 密码加密配置
+# ------------------------------
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+# ------------------------------
+# JWT 配置
+# ------------------------------
+SECRET_KEY = JWT_CONFIG["secret_key"]
+ALGORITHM = JWT_CONFIG["algorithm"]
+ACCESS_TOKEN_EXPIRE_MINUTES = int(JWT_CONFIG["access_token_expire_minutes"])
+
+# OAuth2 依赖(从请求头获取 Token、格式: Bearer )
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/login")
+
+
+# ------------------------------
+# 密码工具函数
+# ------------------------------
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """验证明文密码与加密密码是否匹配"""
+ return pwd_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+ """对明文密码进行 bcrypt 加密"""
+ return pwd_context.hash(password)
+
+
+# ------------------------------
+# JWT 工具函数
+# ------------------------------
+def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
+ """生成 JWT Token"""
+ to_encode = data.copy()
+ # 设置过期时间
+ if expires_delta:
+ expire = datetime.now(timezone.utc) + expires_delta
+ else:
+ expire = datetime.now(timezone.utc) + timedelta(minutes=15)
+ # 添加过期时间到 Token 数据
+ to_encode.update({"exp": expire})
+ # 生成 Token
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
+ return encoded_jwt
+
+
+# ------------------------------
+# 认证依赖(获取当前登录用户)
+# ------------------------------
+def get_current_user(token: str = Depends(oauth2_scheme)): # 移除返回类型注解
+ """从 Token 中解析用户信息、验证通过后返回当前用户"""
+ # 延迟导入、打破循环依赖
+ from schema.user_schema import UserResponse # 在这里导入
+
+ # 认证失败异常
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Token 无效或已过期",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ try:
+ # 解码 Token
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+ # 获取 Token 中的用户名
+ username: str = payload.get("sub")
+ if username is None:
+ raise credentials_exception
+ except JWTError:
+ raise credentials_exception
+
+ # 从数据库查询用户(验证用户是否存在)
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ query = "SELECT id, username, created_at, updated_at FROM users WHERE username = %s"
+ cursor.execute(query, (username,))
+ user = cursor.fetchone()
+
+ if user is None:
+ raise credentials_exception
+
+ # 转换为 UserResponse 模型(自动校验字段)
+ return UserResponse(**user)
+ except Exception as e:
+ raise credentials_exception from e
+ finally:
+ db.close_connection(conn, cursor)
diff --git a/middle/error_handler.py b/middle/error_handler.py
new file mode 100644
index 0000000..e832b6b
--- /dev/null
+++ b/middle/error_handler.py
@@ -0,0 +1,68 @@
+from fastapi import Request, status
+from fastapi.responses import JSONResponse
+from fastapi.exceptions import HTTPException, RequestValidationError
+from mysql.connector import Error as MySQLError
+from jose import JWTError
+
+from schema.response_schema import APIResponse
+
+
+async def global_exception_handler(request: Request, exc: Exception):
+ """全局异常处理器: 所有未捕获的异常都会在这里统一处理"""
+ # 请求参数验证错误(Pydantic 校验失败)
+ if isinstance(exc, RequestValidationError):
+ error_details = []
+ for err in exc.errors():
+ error_details.append(f"{err['loc'][1]}: {err['msg']}")
+ return JSONResponse(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ content=APIResponse(
+ code=400,
+ message=f"请求参数错误: {'; '.join(error_details)}",
+ data=None
+ ).model_dump()
+ )
+
+ # HTTP 异常(主动抛出的业务错误、如 401/404)
+ if isinstance(exc, HTTPException):
+ return JSONResponse(
+ status_code=exc.status_code,
+ content=APIResponse(
+ code=exc.status_code,
+ message=exc.detail,
+ data=None
+ ).model_dump()
+ )
+
+ # JWT 相关错误(Token 无效/过期)
+ if isinstance(exc, JWTError):
+ return JSONResponse(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ content=APIResponse(
+ code=401,
+ message="Token 无效或已过期",
+ data=None
+ ).model_dump(),
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ # MySQL 数据库错误
+ if isinstance(exc, MySQLError):
+ return JSONResponse(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ content=APIResponse(
+ code=500,
+ message=f"数据库错误: {str(exc)}",
+ data=None
+ ).model_dump()
+ )
+
+ # 其他未知错误
+ return JSONResponse(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ content=APIResponse(
+ code=500,
+ message=f"服务器内部错误: {str(exc)}",
+ data=None
+ ).model_dump()
+ )
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..f510264
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,21 @@
+fastapi==0.116.1
+insightface==0.7.3
+numpy==1.26.1
+opencv_contrib_python==4.6.0.66
+opencv_python==4.8.1.78
+opencv_python_headless==4.11.0.86
+paddleocr==2.6.0.1
+paddlepaddle-gpu==2.5.2
+passlib==1.7.4
+Pillow==11.3.0
+pycryptodome==3.23.0
+pydantic==2.11.7
+python_jose==3.5.0
+torch==2.3.1+cu118
+torchaudio==2.3.1+cu118
+torchvision==0.18.1+cu118
+ultralytics==8.3.198
+uvicorn==0.35.0
+insightface==0.7.3
+onnxruntime-gpu==1.15.1
+uvicorn[standard]
\ No newline at end of file
diff --git a/router/__pycache__/device_danger_router.cpython-310.pyc b/router/__pycache__/device_danger_router.cpython-310.pyc
new file mode 100644
index 0000000..62e2fa3
Binary files /dev/null and b/router/__pycache__/device_danger_router.cpython-310.pyc differ
diff --git a/router/__pycache__/device_router.cpython-310.pyc b/router/__pycache__/device_router.cpython-310.pyc
new file mode 100644
index 0000000..0c79621
Binary files /dev/null and b/router/__pycache__/device_router.cpython-310.pyc differ
diff --git a/router/__pycache__/face_router.cpython-310.pyc b/router/__pycache__/face_router.cpython-310.pyc
new file mode 100644
index 0000000..2c4a978
Binary files /dev/null and b/router/__pycache__/face_router.cpython-310.pyc differ
diff --git a/router/__pycache__/file_router.cpython-310.pyc b/router/__pycache__/file_router.cpython-310.pyc
new file mode 100644
index 0000000..86f128c
Binary files /dev/null and b/router/__pycache__/file_router.cpython-310.pyc differ
diff --git a/router/__pycache__/model_router.cpython-310.pyc b/router/__pycache__/model_router.cpython-310.pyc
new file mode 100644
index 0000000..bd75386
Binary files /dev/null and b/router/__pycache__/model_router.cpython-310.pyc differ
diff --git a/router/__pycache__/sensitive_router.cpython-310.pyc b/router/__pycache__/sensitive_router.cpython-310.pyc
new file mode 100644
index 0000000..e413b93
Binary files /dev/null and b/router/__pycache__/sensitive_router.cpython-310.pyc differ
diff --git a/router/__pycache__/user_router.cpython-310.pyc b/router/__pycache__/user_router.cpython-310.pyc
new file mode 100644
index 0000000..9bf1bcf
Binary files /dev/null and b/router/__pycache__/user_router.cpython-310.pyc differ
diff --git a/router/device_action_router.py b/router/device_action_router.py
new file mode 100644
index 0000000..9f19e61
--- /dev/null
+++ b/router/device_action_router.py
@@ -0,0 +1,112 @@
+from fastapi import APIRouter, Query, Path
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from schema.device_action_schema import (
+ DeviceActionResponse,
+ DeviceActionListResponse
+)
+from schema.response_schema import APIResponse
+
+# 路由配置
+router = APIRouter(
+ prefix="/api/device/actions",
+ tags=["设备操作记录"]
+)
+
+@router.get("/list", response_model=APIResponse, summary="分页查询设备操作记录")
+@encrypt_response()
+async def get_device_action_list(
+ page: int = Query(1, ge=1, description="页码、默认1"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100"),
+ client_ip: str = Query(None, description="按客户端IP筛选"),
+ action: int = Query(None, ge=0, le=1, description="按状态筛选(0=离线、1=上线)")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ # 构建筛选条件(参数化查询、避免注入)
+ where_clause = []
+ params = []
+ if client_ip:
+ where_clause.append("client_ip = %s")
+ params.append(client_ip)
+ if action is not None:
+ where_clause.append("action = %s")
+ params.append(action)
+
+ # 查询总记录数(用于返回 total)
+ count_sql = "SELECT COUNT(*) AS total FROM device_action"
+ if where_clause:
+ count_sql += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_sql, params)
+ total = cursor.fetchone()["total"]
+
+ # 分页查询记录(按创建时间倒序、确保最新记录在前)
+ offset = (page - 1) * page_size
+ list_sql = "SELECT * FROM device_action"
+ if where_clause:
+ list_sql += " WHERE " + " AND ".join(where_clause)
+ list_sql += " ORDER BY created_at DESC LIMIT %s OFFSET %s"
+ params.extend([page_size, offset])
+
+ cursor.execute(list_sql, params)
+ action_list = cursor.fetchall()
+
+ # 仅返回 total + device_actions
+ return APIResponse(
+ code=200,
+ message="查询成功",
+ data=DeviceActionListResponse(
+ total=total,
+ device_actions=[DeviceActionResponse(**item) for item in action_list]
+ )
+ )
+
+ except MySQLError as e:
+ raise Exception(f"查询记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+@router.get("/{client_ip}", response_model=APIResponse, summary="根据IP查询设备操作记录")
+@encrypt_response()
+async def get_device_actions_by_ip(
+ client_ip: str = Path(..., description="客户端IP地址")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 1. 查询总记录数
+ count_sql = "SELECT COUNT(*) AS total FROM device_action WHERE client_ip = %s"
+ cursor.execute(count_sql, (client_ip,))
+ total = cursor.fetchone()["total"]
+
+ # 2. 查询该IP的所有记录(按创建时间倒序)
+ list_sql = """
+ SELECT * FROM device_action
+ WHERE client_ip = %s
+ ORDER BY created_at DESC
+ """
+ cursor.execute(list_sql, (client_ip,))
+ action_list = cursor.fetchall()
+
+ # 3. 返回结果
+ return APIResponse(
+ code=200,
+ message="查询成功",
+ data=DeviceActionListResponse(
+ total=total,
+ device_actions=[DeviceActionResponse(**item) for item in action_list]
+ )
+ )
+
+ except MySQLError as e:
+ raise Exception(f"查询记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
diff --git a/router/device_danger_router.py b/router/device_danger_router.py
new file mode 100644
index 0000000..ad8693b
--- /dev/null
+++ b/router/device_danger_router.py
@@ -0,0 +1,164 @@
+from datetime import date
+
+from fastapi import APIRouter, Query, HTTPException, Path
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from schema.device_danger_schema import (
+ DeviceDangerResponse, DeviceDangerListResponse
+)
+from schema.response_schema import APIResponse
+
+router = APIRouter(
+ prefix="/api/devices/dangers",
+ tags=["设备管理-危险记录"]
+)
+
+# 获取危险记录列表
+@router.get("/", response_model=APIResponse, summary="获取设备危险记录列表(多条件筛选)")
+@encrypt_response()
+async def get_danger_list(
+ page: int = Query(1, ge=1, description="页码、默认第1页"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间"),
+ client_ip: str = Query(None, max_length=100, description="按设备IP筛选"),
+ danger_type: str = Query(None, max_length=255, alias="type", description="按危险类型筛选"),
+ start_date: date = Query(None, description="按创建时间筛选(开始日期、格式YYYY-MM-DD)"),
+ end_date: date = Query(None, description="按创建时间筛选(结束日期、格式YYYY-MM-DD)")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 构建筛选条件
+ where_clause = []
+ params = []
+
+ if client_ip:
+ where_clause.append("client_ip = %s")
+ params.append(client_ip)
+ if danger_type:
+ where_clause.append("type = %s")
+ params.append(danger_type)
+ if start_date:
+ where_clause.append("DATE(created_at) >= %s")
+ params.append(start_date.strftime("%Y-%m-%d"))
+ if end_date:
+ where_clause.append("DATE(created_at) <= %s")
+ params.append(end_date.strftime("%Y-%m-%d"))
+
+ # 1. 统计符合条件的总记录数
+ count_query = "SELECT COUNT(*) AS total FROM device_danger"
+ if where_clause:
+ count_query += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_query, params)
+ total = cursor.fetchone()["total"]
+
+ # 2. 分页查询记录(按创建时间倒序、最新的在前)
+ offset = (page - 1) * page_size
+ list_query = "SELECT * FROM device_danger"
+ if where_clause:
+ list_query += " WHERE " + " AND ".join(where_clause)
+ list_query += " ORDER BY created_at DESC LIMIT %s OFFSET %s"
+ params.extend([page_size, offset]) # 追加分页参数
+
+ cursor.execute(list_query, params)
+ danger_list = cursor.fetchall()
+
+ # 转换为响应模型
+ return APIResponse(
+ code=200,
+ message="获取危险记录列表成功",
+ data=DeviceDangerListResponse(
+ total=total,
+ dangers=[DeviceDangerResponse(**item) for item in danger_list]
+ )
+ )
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"查询危险记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 获取单个设备的所有危险记录
+@router.get("/device/{client_ip}", response_model=APIResponse, summary="获取单个设备的所有危险记录")
+# @encrypt_response()
+async def get_device_dangers(
+ client_ip: str = Path(..., max_length=100, description="设备IP地址"),
+ page: int = Query(1, ge=1, description="页码、默认第1页"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间")
+):
+ # 先检查设备是否存在
+ from service.device_danger_service import check_device_exist
+ if not check_device_exist(client_ip):
+ raise HTTPException(status_code=404, detail=f"IP为 {client_ip} 的设备不存在")
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 1. 统计该设备的危险记录总数
+ count_query = "SELECT COUNT(*) AS total FROM device_danger WHERE client_ip = %s"
+ cursor.execute(count_query, (client_ip,))
+ total = cursor.fetchone()["total"]
+
+ # 2. 分页查询该设备的危险记录
+ offset = (page - 1) * page_size
+ list_query = """
+ SELECT * FROM device_danger
+ WHERE client_ip = %s
+ ORDER BY created_at DESC
+ LIMIT %s OFFSET %s
+ """
+ cursor.execute(list_query, (client_ip, page_size, offset))
+ danger_list = cursor.fetchall()
+
+ return APIResponse(
+ code=200,
+ message=f"获取设备[{client_ip}]危险记录成功(共{total}条)",
+ data=DeviceDangerListResponse(
+ total=total,
+ dangers=[DeviceDangerResponse(**item) for item in danger_list]
+ )
+ )
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"查询设备[{client_ip}]危险记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# ------------------------------
+# 根据ID获取单个危险记录详情
+# ------------------------------
+@router.get("/{danger_id}", response_model=APIResponse, summary="根据ID获取单个危险记录详情")
+@encrypt_response()
+async def get_danger_detail(
+ danger_id: int = Path(..., ge=1, description="危险记录ID")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 查询单个危险记录
+ query = "SELECT * FROM device_danger WHERE id = %s"
+ cursor.execute(query, (danger_id,))
+ danger = cursor.fetchone()
+
+ if not danger:
+ raise HTTPException(status_code=404, detail=f"ID为 {danger_id} 的危险记录不存在")
+
+ return APIResponse(
+ code=200,
+ message="获取危险记录详情成功",
+ data=DeviceDangerResponse(**danger)
+ )
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"查询危险记录详情失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
\ No newline at end of file
diff --git a/router/device_router.py b/router/device_router.py
new file mode 100644
index 0000000..e6a979b
--- /dev/null
+++ b/router/device_router.py
@@ -0,0 +1,329 @@
+import asyncio
+import json
+from datetime import date
+
+from fastapi import APIRouter, Query, HTTPException, Request, Path
+from mysql.connector import Error as MySQLError
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from schema.device_schema import (
+ DeviceCreateRequest, DeviceResponse, DeviceListResponse,
+ DeviceStatusHistoryResponse, DeviceStatusHistoryListResponse
+)
+from schema.response_schema import APIResponse
+from service.device_service import update_online_status_by_ip
+from ws.ws import get_current_time_str, aes_encrypt, is_client_connected
+
+router = APIRouter(
+ prefix="/api/devices",
+ tags=["设备管理"]
+)
+
+
+# 创建设备信息接口
+@router.post("/add", response_model=APIResponse, summary="创建设备信息")
+@encrypt_response()
+async def create_device(device_data: DeviceCreateRequest, request: Request):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 检查设备是否已存在
+ cursor.execute("SELECT * FROM devices WHERE client_ip = %s", (device_data.ip,))
+ existing_device = cursor.fetchone()
+ if existing_device:
+ # 更新设备为在线状态
+ from service.device_service import update_online_status_by_ip
+ update_online_status_by_ip(client_ip=device_data.ip, online_status=1)
+ return APIResponse(
+ code=200,
+ message=f"设备IP {device_data.ip} 已存在、返回已有设备信息",
+ data=DeviceResponse(**existing_device)
+ )
+
+ # 通过 User-Agent 判断设备类型
+ user_agent = request.headers.get("User-Agent", "").lower()
+ device_type = "unknown"
+ if user_agent == "default":
+ device_type = device_data.params.get("os") if (device_data.params and isinstance(device_data.params, dict)) else "unknown"
+ elif "windows" in user_agent:
+ device_type = "windows"
+ elif "android" in user_agent:
+ device_type = "android"
+ elif "linux" in user_agent:
+ device_type = "linux"
+
+ device_params_json = json.dumps(device_data.params) if device_data.params else None
+
+ # 插入新设备
+ insert_query = """
+ INSERT INTO devices
+ (client_ip, hostname, device_online_status, device_type, alarm_count, params, is_need_handler)
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
+ """
+ cursor.execute(insert_query, (
+ device_data.ip,
+ device_data.hostname,
+ 0,
+ device_type,
+ 0,
+ device_params_json,
+ 0
+ ))
+ conn.commit()
+
+ # 获取新设备并返回
+ device_id = cursor.lastrowid
+ cursor.execute("SELECT * FROM devices WHERE id = %s", (device_id,))
+ new_device = cursor.fetchone()
+
+ return APIResponse(
+ code=200,
+ message="设备创建成功",
+ data=DeviceResponse(**new_device)
+ )
+
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise Exception(f"创建设备失败: {str(e)}") from e
+ except json.JSONDecodeError as e:
+ raise Exception(f"设备参数JSON序列化失败: {str(e)}") from e
+ except Exception as e:
+ if conn:
+ conn.rollback()
+ raise e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# ------------------------------
+# 获取设备列表接口
+# ------------------------------
+@router.get("/", response_model=APIResponse, summary="获取设备列表(支持筛选分页)")
+@encrypt_response()
+async def get_device_list(
+ page: int = Query(1, ge=1, description="页码、默认第1页"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间"),
+ device_type: str = Query(None, description="按设备类型筛选"),
+ online_status: int = Query(None, ge=0, le=1, description="按在线状态筛选")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ where_clause = []
+ params = []
+
+ if device_type:
+ where_clause.append("device_type = %s")
+ params.append(device_type)
+ if online_status is not None:
+ where_clause.append("device_online_status = %s")
+ params.append(online_status)
+
+ # 统计总数
+ count_query = "SELECT COUNT(*) AS total FROM devices"
+ if where_clause:
+ count_query += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_query, params)
+ total = cursor.fetchone()["total"]
+
+ # 分页查询列表
+ offset = (page - 1) * page_size
+ list_query = "SELECT * FROM devices"
+ if where_clause:
+ list_query += " WHERE " + " AND ".join(where_clause)
+ list_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
+ params.extend([page_size, offset])
+
+ cursor.execute(list_query, params)
+ device_list = cursor.fetchall()
+
+ return APIResponse(
+ code=200,
+ message="获取设备列表成功",
+ data=DeviceListResponse(
+ total=total,
+ devices=[DeviceResponse(**device) for device in device_list]
+ )
+ )
+
+ except MySQLError as e:
+ raise Exception(f"获取设备列表失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# ------------------------------
+# 获取设备上下线记录接口
+# ------------------------------
+@router.get("/status-history", response_model=APIResponse, summary="获取设备上下线记录")
+@encrypt_response()
+async def get_device_status_history(
+ client_ip: str = Query(None, description="客户端IP地址(非必填,为空时返回所有设备记录)"),
+ page: int = Query(1, ge=1, description="页码、默认第1页"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间"),
+ start_date: date = Query(None, description="开始日期、格式YYYY-MM-DD"),
+ end_date: date = Query(None, description="结束日期、格式YYYY-MM-DD")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 1. 检查设备是否存在(仅传IP时):强制指定Collation
+ if client_ip is not None:
+ # 关键调整1:WHERE条件中给d.client_ip指定Collation(与da一致或反之)
+ check_query = """
+ SELECT id FROM devices
+ WHERE client_ip COLLATE utf8mb4_general_ci = %s COLLATE utf8mb4_general_ci
+ """
+ cursor.execute(check_query, (client_ip,))
+ device = cursor.fetchone()
+ if not device:
+ raise HTTPException(status_code=404, detail=f"客户端IP为 {client_ip} 的设备不存在")
+
+ # 2. 构建WHERE条件
+ where_clause = []
+ params = []
+
+ # 关键调整2:传IP时,强制指定da.client_ip的Collation
+ if client_ip is not None:
+ where_clause.append("da.client_ip COLLATE utf8mb4_general_ci = %s COLLATE utf8mb4_general_ci")
+ params.append(client_ip)
+ if start_date:
+ where_clause.append("DATE(da.created_at) >= %s")
+ params.append(start_date.strftime("%Y-%m-%d"))
+ if end_date:
+ where_clause.append("DATE(da.created_at) <= %s")
+ params.append(end_date.strftime("%Y-%m-%d"))
+
+ # 3. 统计总数:JOIN时强制统一Collation
+ count_query = """
+ SELECT COUNT(*) AS total
+ FROM device_action da
+ LEFT JOIN devices d
+ ON da.client_ip COLLATE utf8mb4_general_ci = d.client_ip COLLATE utf8mb4_general_ci
+ """
+ if where_clause:
+ count_query += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_query, params)
+ total = cursor.fetchone()["total"]
+
+ # 4. 分页查询:JOIN时强制统一Collation
+ offset = (page - 1) * page_size
+ list_query = """
+ SELECT da.*, d.id AS device_id
+ FROM device_action da
+ LEFT JOIN devices d
+ ON da.client_ip COLLATE utf8mb4_general_ci = d.client_ip COLLATE utf8mb4_general_ci
+ """
+ if where_clause:
+ list_query += " WHERE " + " AND ".join(where_clause)
+ list_query += " ORDER BY da.created_at DESC LIMIT %s OFFSET %s"
+ params.extend([page_size, offset])
+ cursor.execute(list_query, params)
+ history_list = cursor.fetchall()
+
+ # 后续格式化响应逻辑不变...
+ formatted_history = []
+ for item in history_list:
+ formatted_item = {
+ "id": item["id"],
+ "device_id": item["device_id"], # 可能为None(IP无对应设备)
+ "client_ip": item["client_ip"],
+ "status": item["action"],
+ "status_time": item["created_at"]
+ }
+ formatted_history.append(formatted_item)
+
+ return APIResponse(
+ code=200,
+ message="获取设备上下线记录成功",
+ data=DeviceStatusHistoryListResponse(
+ total=total,
+ history=[DeviceStatusHistoryResponse(**item) for item in formatted_history]
+ )
+ )
+
+ except MySQLError as e:
+ raise Exception(f"获取设备上下线记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+# ------------------------------
+# 通过客户端IP设置设备is_need_handler为0接口
+# ------------------------------
+@router.post("/need-handler/reset", response_model=APIResponse, summary="解封客户端")
+@encrypt_response()
+async def reset_device_need_handler(
+ client_ip: str = Query(..., description="目标设备的客户端IP地址(必填)")
+):
+ try:
+ from service.device_service import update_is_need_handler_by_client_ip
+ success = update_is_need_handler_by_client_ip(
+ client_ip=client_ip,
+ is_need_handler=0 # 固定设置为0(不需要处理)
+ )
+
+ if success:
+ online_status = is_client_connected(client_ip)
+
+ # 如果设备在线,则发送消息给前端
+ if online_status:
+ # 调用 ws 发送一个消息给前端、告诉他已解锁
+ unlock_msg = {
+ "type": "unlock",
+ "timestamp": get_current_time_str(),
+ "client_ip": client_ip
+ }
+ from ws.ws import send_message_to_client
+ await send_message_to_client(client_ip, json.dumps(unlock_msg))
+
+ # 休眠 100 ms
+ await asyncio.sleep(0.1)
+
+ frame_permit_msg = {
+ "type": "frame",
+ "timestamp": get_current_time_str(),
+ "client_ip": client_ip
+ }
+ await send_message_to_client(client_ip, json.dumps(frame_permit_msg))
+
+ # 更新设备在线状态为1
+ update_online_status_by_ip(client_ip, 1)
+
+ return APIResponse(
+ code=200,
+ message=f"设备已解封",
+ data={
+ "client_ip": client_ip,
+ "is_need_handler": 0,
+ "status_desc": "设备已解封"
+ }
+ )
+
+ # 捕获工具方法抛出的业务异常(如IP为空、设备不存在)
+ except ValueError as e:
+ # 业务异常返回400/404状态码(与现有接口异常规范一致)
+ raise HTTPException(
+ status_code=404 if "设备不存在" in str(e) else 400,
+ detail=str(e)
+ ) from e
+
+ # 捕获数据库层面异常(如连接失败、SQL执行错误)
+ except MySQLError as e:
+ raise Exception(f"设置is_need_handler失败:数据库操作异常 - {str(e)}") from e
+
+ # 捕获其他未知异常
+ except Exception as e:
+ raise Exception(f"设置is_need_handler失败:未知错误 - {str(e)}") from e
+
+
+
diff --git a/router/face_router.py b/router/face_router.py
new file mode 100644
index 0000000..d32ca6a
--- /dev/null
+++ b/router/face_router.py
@@ -0,0 +1,326 @@
+from io import BytesIO
+from pathlib import Path
+
+from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Query, Request
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from schema.face_schema import (
+ FaceCreateRequest,
+ FaceResponse,
+ FaceListResponse
+)
+from schema.response_schema import APIResponse
+from service.face_service import update_face_data
+from util.face_util import add_binary_data
+from service.file_service import save_source_file
+
+router = APIRouter(prefix="/api/faces", tags=["人脸管理"])
+
+
+# 创建人脸记录
+@router.post("", response_model=APIResponse, summary="创建人脸记录")
+@encrypt_response()
+async def create_face(
+ request: Request,
+ name: str = Form(None, max_length=255, description="名称(可选)"),
+ file: UploadFile = File(..., description="人脸文件(必传)")
+):
+ conn = None
+ cursor = None
+ try:
+ face_create = FaceCreateRequest(name=name)
+ client_ip = request.client.host if request.client else ""
+ if not client_ip:
+ raise HTTPException(status_code=400, detail="无法获取客户端IP")
+
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ # 先读取文件内容
+ file_content = await file.read()
+ # 将文件指针重置到开头
+ await file.seek(0)
+ # 再保存文件
+ path = save_source_file(file, "face")
+ # 提取人脸特征
+ detect_success, detect_result = add_binary_data(file_content)
+ if not detect_success:
+ raise HTTPException(status_code=400, detail=f"人脸检测失败:{detect_result}")
+ eigenvalue = detect_result
+
+ # 插入数据库
+ insert_query = """
+ INSERT INTO face (name, eigenvalue, address)
+ VALUES (%s, %s, %s)
+ """
+ cursor.execute(insert_query, (face_create.name, str(eigenvalue), path))
+ conn.commit()
+
+ # 查询新记录
+ cursor.execute("""
+ SELECT id, name, address, created_at, updated_at
+ FROM face
+ WHERE id = LAST_INSERT_ID()
+ """)
+ created_face = cursor.fetchone()
+ if not created_face:
+ raise HTTPException(status_code=500, detail="创建成功但无法获取记录")
+
+
+ # TODO 重新加载人脸模型
+ update_face_data()
+
+
+ return APIResponse(
+ code=200,
+ message=f"人脸记录创建成功(ID: {created_face['id']})",
+ data=FaceResponse(**created_face)
+ )
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(status_code=500, detail=f"创建失败: {str(e)}") from e
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"服务器错误: {str(e)}") from e
+ finally:
+ await file.close()
+ db.close_connection(conn, cursor)
+
+
+# 获取单个人脸记录
+@router.get("/{face_id}", response_model=APIResponse, summary="获取单个人脸记录")
+@encrypt_response()
+async def get_face(face_id: int):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ query = """
+ SELECT id, name, address, created_at, updated_at
+ FROM face
+ WHERE id = %s
+ """
+ cursor.execute(query, (face_id,))
+ face = cursor.fetchone()
+
+ if not face:
+ raise HTTPException(status_code=404, detail=f"ID为 {face_id} 的记录不存在")
+
+ return APIResponse(
+ code=200,
+ message="查询成功",
+ data=FaceResponse(**face)
+ )
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 获取人脸列表
+@router.get("", response_model=APIResponse, summary="获取人脸列表(分页+筛选)")
+@encrypt_response()
+async def get_face_list(
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ name: str = Query(None),
+ has_eigenvalue: bool = Query(None)
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ where_clause = []
+ params = []
+ if name:
+ where_clause.append("name LIKE %s")
+ params.append(f"%{name}%")
+ if has_eigenvalue is not None:
+ where_clause.append("eigenvalue IS NOT NULL" if has_eigenvalue else "eigenvalue IS NULL")
+
+ # 总记录数
+ count_query = "SELECT COUNT(*) AS total FROM face"
+ if where_clause:
+ count_query += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_query, params)
+ total = cursor.fetchone()["total"]
+
+ # 列表数据
+ offset = (page - 1) * page_size
+ list_query = """
+ SELECT id, name, address, created_at, updated_at
+ FROM face
+ """
+ if where_clause:
+ list_query += " WHERE " + " AND ".join(where_clause)
+ list_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
+ params.extend([page_size, offset])
+
+ cursor.execute(list_query, params)
+ face_list = cursor.fetchall()
+
+ return APIResponse(
+ code=200,
+ message=f"获取成功(共{total}条)",
+ data=FaceListResponse(
+ total=total,
+ faces=[FaceResponse(**face) for face in face_list]
+ )
+ )
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 删除人脸记录
+@router.delete("/{face_id}", response_model=APIResponse, summary="删除人脸记录")
+@encrypt_response()
+async def delete_face(face_id: int):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ cursor.execute("SELECT id, address FROM face WHERE id = %s", (face_id,))
+ exist_face = cursor.fetchone()
+ if not exist_face:
+ raise HTTPException(status_code=404, detail=f"ID为 {face_id} 的记录不存在")
+ old_db_path = exist_face["address"]
+
+ cursor.execute("DELETE FROM face WHERE id = %s", (face_id,))
+ conn.commit()
+
+ # 删除图片
+ if old_db_path:
+ old_abs_path = Path(old_db_path).resolve()
+ if old_abs_path.exists():
+ try:
+ old_abs_path.unlink()
+ print(f"[FaceRouter] 已删除图片:{old_abs_path}")
+ extra_msg = "(已同步删除图片)"
+ except Exception as e:
+ print(f"[FaceRouter] 删除图片失败:{str(e)}")
+ extra_msg = "(图片删除失败)"
+ else:
+ extra_msg = "(图片不存在)"
+ else:
+ extra_msg = "(无关联图片)"
+
+
+ # TODO 重新加载人脸模型
+ update_face_data()
+
+ return APIResponse(
+ code=200,
+ message=f"ID为 {face_id} 的记录删除成功 {extra_msg}",
+ data=None
+ )
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+@router.post("/batch-import", response_model=APIResponse, summary="批量导入文件夹下的人脸图片")
+# @encrypt_response()
+async def batch_import_faces(
+ folder_path: str = Form(..., description="人脸图片所在的**服务器本地文件夹路径**")
+):
+ conn = None
+ cursor = None
+ success_count = 0 # 成功导入数量
+ fail_list = [] # 失败记录(包含文件名、错误原因)
+ try:
+ # 1. 验证文件夹有效性
+ folder = Path(folder_path)
+ if not folder.exists() or not folder.is_dir():
+ raise HTTPException(status_code=400, detail=f"文件夹 {folder_path} 不存在或不是有效目录")
+
+ # 2. 定义支持的图片格式
+ supported_extensions = {".png", ".jpg", ".jpeg", ".webp"}
+
+ # 3. 数据库连接初始化
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 4. 遍历文件夹内所有文件
+ for file_path in folder.iterdir():
+ if file_path.is_file() and file_path.suffix.lower() in supported_extensions:
+ file_name = file_path.stem # 提取文件名(不含后缀)作为 `name`
+ try:
+ # 4.1 读取文件二进制内容
+ with open(file_path, "rb") as f:
+ file_content = f.read()
+
+ # 4.2 构造模拟的 UploadFile 对象(用于兼容 `save_source_file`)
+ mock_file = UploadFile(
+ filename=file_path.name,
+ file=BytesIO(file_content)
+ )
+
+ # 4.3 保存文件到指定目录
+ saved_path = save_source_file(mock_file, "face")
+
+ # 4.4 提取人脸特征
+ detect_success, detect_result = add_binary_data(file_content)
+ if not detect_success:
+ fail_list.append({
+ "name": file_name,
+ "file_path": str(file_path),
+ "error": f"人脸检测失败:{detect_result}"
+ })
+ continue # 跳过当前文件,处理下一个
+ eigenvalue = detect_result
+
+ # 4.5 插入数据库
+ insert_sql = """
+ INSERT INTO face (name, eigenvalue, address)
+ VALUES (%s, %s, %s)
+ """
+ cursor.execute(insert_sql, (file_name, str(eigenvalue), saved_path))
+ conn.commit() # 提交当前文件的插入操作
+
+ success_count += 1
+
+ except Exception as e:
+ # 捕获单文件处理的异常,记录后继续处理其他文件
+ fail_list.append({
+ "name": file_name,
+ "file_path": str(file_path),
+ "error": str(e)
+ })
+ if conn:
+ conn.rollback() # 回滚当前失败文件的插入
+
+ # 5. 重新加载人脸模型(确保新增数据生效)
+ update_face_data()
+
+ # 6. 构造返回结果
+ return APIResponse(
+ code=200,
+ message=f"批量导入完成,成功 {success_count} 条,失败 {len(fail_list)} 条",
+ data={
+ "success_count": success_count,
+ "fail_details": fail_list
+ }
+ )
+
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(status_code=500, detail=f"数据库操作失败: {str(e)}") from e
+ except HTTPException:
+ raise # 直接抛出400等由业务逻辑触发的HTTP异常
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
\ No newline at end of file
diff --git a/router/file_router.py b/router/file_router.py
new file mode 100644
index 0000000..838c090
--- /dev/null
+++ b/router/file_router.py
@@ -0,0 +1,31 @@
+import os
+from fastapi import FastAPI, HTTPException, Path, APIRouter
+from fastapi.responses import FileResponse
+from service.file_service import UPLOAD_ROOT
+
+router = APIRouter(
+ prefix="/api/file",
+ tags=["文件管理"]
+)
+
+
+@router.get("/download/{relative_path:path}", summary="下载文件")
+async def download_file(
+ relative_path: str = Path(..., description="文件的相对路径")
+):
+ file_path = os.path.abspath(os.path.join(UPLOAD_ROOT, relative_path))
+
+ if not os.path.exists(file_path):
+ raise HTTPException(status_code=404, detail=f"文件不存在: {file_path}")
+
+ if not os.path.isfile(file_path):
+ raise HTTPException(status_code=400, detail="路径指向的不是文件")
+
+ if not file_path.startswith(os.path.abspath(UPLOAD_ROOT)):
+ raise HTTPException(status_code=403, detail="无权访问该文件")
+
+ return FileResponse(
+ path=file_path,
+ filename=os.path.basename(file_path),
+ media_type="application/octet-stream"
+ )
\ No newline at end of file
diff --git a/router/model_router.py b/router/model_router.py
new file mode 100644
index 0000000..1cccfbc
--- /dev/null
+++ b/router/model_router.py
@@ -0,0 +1,269 @@
+import os
+from pathlib import Path
+from service.file_service import save_source_file
+
+from fastapi import APIRouter, HTTPException, UploadFile, File, Form, Query
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from schema.model_schema import (
+ ModelResponse,
+ ModelListResponse
+)
+from schema.response_schema import APIResponse
+from service.model_service import ALLOWED_MODEL_EXT, MAX_MODEL_SIZE, load_yolo_model
+
+router = APIRouter(prefix="/api/models", tags=["模型管理"])
+
+
+# 上传模型
+@router.post("", response_model=APIResponse, summary="上传YOLO模型(.pt格式)")
+@encrypt_response()
+async def upload_model(
+ name: str = Form(..., description="模型名称"),
+ description: str = Form(None, description="模型描述"),
+ file: UploadFile = File(..., description=f"YOLO模型文件(.pt、最大{MAX_MODEL_SIZE // 1024 // 1024}MB)")
+):
+ conn = None
+ cursor = None
+ try:
+ # 校验文件格式
+ file_ext = file.filename.split(".")[-1].lower() if "." in file.filename else ""
+ if file_ext not in ALLOWED_MODEL_EXT:
+ raise HTTPException(
+ status_code=400,
+ detail=f"仅支持{ALLOWED_MODEL_EXT}格式、当前:{file_ext}"
+ )
+
+ # 校验文件大小
+ if file.size > MAX_MODEL_SIZE:
+ raise HTTPException(
+ status_code=400,
+ detail=f"文件过大!最大{MAX_MODEL_SIZE // 1024 // 1024}MB、当前{file.size // 1024 // 1024}MB"
+ )
+ # 保存文件
+ file_path = save_source_file(file, "model")
+
+ # 数据库操作
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ insert_sql = """
+ INSERT INTO model (name, path, is_default, description, file_size)
+ VALUES (%s, %s, 0, %s, %s)
+ """
+ cursor.execute(insert_sql, (name, file_path, description, file.size))
+ conn.commit()
+
+ # 获取新增记录
+ cursor.execute("SELECT * FROM model WHERE id = LAST_INSERT_ID()")
+ new_model = cursor.fetchone()
+ if not new_model:
+ raise HTTPException(status_code=500, detail="上传成功但无法获取记录")
+
+ return APIResponse(
+ code=200,
+ message=f"模型上传成功",
+ data=ModelResponse(**new_model)
+ )
+
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(status_code=500, detail=f"数据库错误:{str(e)}") from e
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"服务器错误:{str(e)}") from e
+ finally:
+ await file.close()
+ db.close_connection(conn, cursor)
+
+
+# 获取模型列表
+@router.get("", response_model=APIResponse, summary="获取模型列表(分页)")
+@encrypt_response()
+async def get_model_list(
+ page: int = Query(1, ge=1),
+ page_size: int = Query(10, ge=1, le=100),
+ name: str = Query(None),
+ is_default: bool = Query(None)
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ where_clause = []
+ params = []
+ if name:
+ where_clause.append("name LIKE %s")
+ params.append(f"%{name}%")
+ if is_default is not None:
+ where_clause.append("is_default = %s")
+ params.append(1 if is_default else 0)
+
+ # 总记录数
+ count_sql = "SELECT COUNT(*) AS total FROM model"
+ if where_clause:
+ count_sql += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_sql, params)
+ total = cursor.fetchone()["total"]
+
+ # 分页数据
+ offset = (page - 1) * page_size
+ list_sql = "SELECT * FROM model"
+ if where_clause:
+ list_sql += " WHERE " + " AND ".join(where_clause)
+ list_sql += " ORDER BY updated_at DESC LIMIT %s OFFSET %s"
+ params.extend([page_size, offset])
+
+ cursor.execute(list_sql, params)
+ model_list = cursor.fetchall()
+
+ return APIResponse(
+ code=200,
+ message=f"获取成功!",
+ data=ModelListResponse(
+ total=total,
+ models=[ModelResponse(**model) for model in model_list]
+ )
+ )
+
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"数据库错误:{str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 更换默认模型
+@router.put("/{model_id}/set-default", response_model=APIResponse, summary="更换默认模型")
+@encrypt_response()
+async def set_default_model(
+ model_id: int
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ conn.autocommit = False
+
+ # 校验目标模型是否存在
+ cursor.execute("SELECT * FROM model WHERE id = %s", (model_id,))
+ target_model = cursor.fetchone()
+ if not target_model:
+ raise HTTPException(status_code=404, detail=f"目标模型不存在!")
+
+ # 检查是否已为默认模型
+ if target_model["is_default"]:
+ return APIResponse(
+ code=200,
+ message=f"已是默认模型、无需更换",
+ data=ModelResponse(**target_model)
+ )
+
+ # 数据库事务:更新默认模型状态
+ try:
+ cursor.execute("UPDATE model SET is_default = 0, updated_at = CURRENT_TIMESTAMP")
+ cursor.execute(
+ "UPDATE model SET is_default = 1, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
+ (model_id,)
+ )
+ conn.commit()
+ except MySQLError as e:
+ conn.rollback()
+ raise HTTPException(
+ status_code=500,
+ detail=f"更新默认模型状态失败(已回滚):{str(e)}"
+ ) from e
+
+ # 更新模型
+ load_yolo_model()
+ # 返回成功响应
+ return APIResponse(
+ code=200,
+ message=f"更换成功",
+ data=None
+ )
+
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(status_code=500, detail=f"数据库错误:{str(e)}") from e
+ finally:
+ if conn:
+ conn.autocommit = True
+ db.close_connection(conn, cursor)
+
+
+# 路由文件(如 model_router.py)中的删除接口
+@router.delete("/{model_id}", response_model=APIResponse, summary="删除模型")
+@encrypt_response()
+async def delete_model(model_id: int):
+ # 1. 正确导入 model_service 中的全局变量(关键修复:变量名匹配)
+ from service.model_service import (
+ current_yolo_model,
+ current_model_absolute_path,
+ load_yolo_model # 用于删除后重新加载模型(可选)
+ )
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 2. 查询待删除模型信息
+ cursor.execute("SELECT * FROM model WHERE id = %s", (model_id,))
+ exist_model = cursor.fetchone()
+ if not exist_model:
+ raise HTTPException(status_code=404, detail=f"模型不存在!")
+
+ # 3. 关键判断:①默认模型不可删 ②正在使用的模型不可删
+ if exist_model["is_default"]:
+ raise HTTPException(status_code=400, detail="默认模型不可删除!")
+
+ # 计算待删除模型的绝对路径(与 model_service 逻辑一致)
+ from service.file_service import get_absolute_path
+ del_model_abs_path = get_absolute_path(exist_model["path"])
+
+ # 判断是否正在使用(对比 current_model_absolute_path)
+ if current_model_absolute_path and del_model_abs_path == current_model_absolute_path:
+ raise HTTPException(status_code=400, detail="该模型正在使用中,禁止删除!")
+
+ # 4. 先删除数据库记录(避免文件删除失败导致数据不一致)
+ cursor.execute("DELETE FROM model WHERE id = %s", (model_id,))
+ conn.commit()
+
+ # 5. 再删除本地文件(捕获文件删除异常,不影响数据库删除结果)
+ extra_msg = ""
+ try:
+ if os.path.exists(del_model_abs_path):
+ os.remove(del_model_abs_path) # 或用 Path(del_model_abs_path).unlink()
+ extra_msg = "(本地文件已同步删除)"
+ else:
+ extra_msg = "(本地文件不存在,无需删除)"
+ except Exception as e:
+ extra_msg = f"(本地文件删除失败:{str(e)})"
+
+ # 6. 若删除后当前模型为空(极端情况),重新加载默认模型(可选优化)
+ if current_yolo_model is None:
+ try:
+ load_yolo_model()
+ print(f"[模型删除后] 重新加载默认模型成功")
+ except Exception as e:
+ print(f"[模型删除后] 重新加载默认模型失败:{str(e)}")
+
+ return APIResponse(
+ code=200,
+ message=f"模型删除成功!",
+ data=None
+ )
+
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(status_code=500, detail=f"数据库错误:{str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
diff --git a/router/sensitive_router.py b/router/sensitive_router.py
new file mode 100644
index 0000000..4d006cc
--- /dev/null
+++ b/router/sensitive_router.py
@@ -0,0 +1,306 @@
+from fastapi import APIRouter, Depends, HTTPException, Query, File, UploadFile
+from mysql.connector import Error as MySQLError
+from typing import Optional
+
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from schema.sensitive_schema import (
+ SensitiveCreateRequest,
+ SensitiveResponse,
+ SensitiveListResponse
+)
+from schema.response_schema import APIResponse
+from middle.auth_middleware import get_current_user
+from schema.user_schema import UserResponse
+from service.ocr_service import set_forbidden_words
+from service.sensitive_service import get_all_sensitive_words
+
+router = APIRouter(
+ prefix="/api/sensitives",
+ tags=["敏感信息管理"]
+)
+
+
+# 创建敏感信息记录
+@router.post("", response_model=APIResponse, summary="创建敏感信息记录")
+@encrypt_response()
+async def create_sensitive(
+ sensitive: SensitiveCreateRequest,
+ current_user: UserResponse = Depends(get_current_user)
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 插入新敏感信息记录到数据库(不包含ID、由数据库自动生成)
+ insert_query = """
+ INSERT INTO sensitives (name, created_at, updated_at)
+ VALUES (%s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ """
+ cursor.execute(insert_query, (sensitive.name,))
+ conn.commit()
+
+ # 获取刚插入记录的ID(使用LAST_INSERT_ID()函数)
+ new_id = cursor.lastrowid
+
+ # 查询刚创建的记录并返回
+ select_query = "SELECT * FROM sensitives WHERE id = %s"
+ cursor.execute(select_query, (new_id,))
+ created_sensitive = cursor.fetchone()
+
+ # 重新加载最新的敏感词
+ set_forbidden_words(get_all_sensitive_words())
+
+ return APIResponse(
+ code=200,
+ message="敏感信息记录创建成功",
+ data=SensitiveResponse(**created_sensitive)
+ )
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(
+ status_code=500,
+ detail=f"创建敏感信息记录失败: {str(e)}"
+ ) from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 获取敏感信息分页列表
+@router.get("", response_model=APIResponse, summary="获取敏感信息分页列表(支持关键词搜索)")
+@encrypt_response()
+async def get_sensitive_list(
+ page: int = Query(1, ge=1, description="页码(默认1、最小1)"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数(默认10、1-100)"),
+ name: Optional[str] = Query(None, description="敏感词关键词搜索(模糊匹配)")
+):
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 1. 构建查询条件(支持关键词搜索)
+ where_clause = []
+ params = []
+ if name:
+ where_clause.append("name LIKE %s")
+ params.append(f"%{name}%") # 模糊匹配关键词
+
+ # 2. 查询总记录数(用于分页计算)
+ count_sql = "SELECT COUNT(*) AS total FROM sensitives"
+ if where_clause:
+ count_sql += " WHERE " + " AND ".join(where_clause)
+ cursor.execute(count_sql, params.copy()) # 复制参数列表、避免后续污染
+ total = cursor.fetchone()["total"]
+
+ # 3. 计算分页偏移量
+ offset = (page - 1) * page_size
+
+ # 4. 分页查询敏感词数据(按更新时间倒序、最新的在前)
+ list_sql = "SELECT * FROM sensitives"
+ if where_clause:
+ list_sql += " WHERE " + " AND ".join(where_clause)
+ # 排序+分页(LIMIT 条数 OFFSET 偏移量)
+ list_sql += " ORDER BY updated_at DESC LIMIT %s OFFSET %s"
+ # 补充分页参数(page_size和offset)
+ params.extend([page_size, offset])
+
+ cursor.execute(list_sql, params)
+ sensitive_list = cursor.fetchall()
+
+ # 5. 构造分页响应数据
+ return APIResponse(
+ code=200,
+ message=f"敏感信息列表查询成功(共{total}条记录、当前第{page}页)",
+ data=SensitiveListResponse(
+ total=total,
+ sensitives=[SensitiveResponse(**item) for item in sensitive_list]
+ )
+ )
+ except MySQLError as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"查询敏感信息列表失败: {str(e)}"
+ ) from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 删除敏感信息记录
+@router.delete("/{sensitive_id}", response_model=APIResponse, summary="删除敏感信息记录")
+@encrypt_response()
+async def delete_sensitive(
+ sensitive_id: int,
+ current_user: UserResponse = Depends(get_current_user) # 需登录认证
+):
+ """
+ 删除敏感信息记录:
+ - 需登录认证
+ - 根据ID删除敏感信息记录
+ - 返回删除成功信息
+ """
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 1. 检查记录是否存在
+ check_query = "SELECT id FROM sensitives WHERE id = %s"
+ cursor.execute(check_query, (sensitive_id,))
+ existing_sensitive = cursor.fetchone()
+ if not existing_sensitive:
+ raise HTTPException(
+ status_code=404,
+ detail=f"ID为 {sensitive_id} 的敏感信息记录不存在"
+ )
+
+ # 2. 执行删除操作
+ delete_query = "DELETE FROM sensitives WHERE id = %s"
+ cursor.execute(delete_query, (sensitive_id,))
+ conn.commit()
+
+ # 重新加载最新的敏感词
+ set_forbidden_words(get_all_sensitive_words())
+
+ return APIResponse(
+ code=200,
+ message=f"ID为 {sensitive_id} 的敏感信息记录删除成功",
+ data=None
+ )
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(
+ status_code=500,
+ detail=f"删除敏感信息记录失败: {str(e)}"
+ ) from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 批量导入敏感信息(从txt文件)
+@router.post("/batch-import", response_model=APIResponse, summary="批量导入敏感信息(从txt文件)")
+@encrypt_response()
+async def batch_import_sensitives(
+ file: UploadFile = File(..., description="包含敏感词的txt文件,每行一个敏感词"),
+ # current_user: UserResponse = Depends(get_current_user) # 添加认证依赖
+):
+ """
+ 批量导入敏感信息:
+ - 需登录认证
+ - 接收txt文件,文件中每行一个敏感词
+ - 批量插入到数据库中(仅插入不存在的敏感词)
+ - 返回导入结果统计
+ """
+ # 检查文件类型
+ filename = file.filename or ""
+ if not filename.lower().endswith(".txt"):
+ raise HTTPException(
+ status_code=400,
+ detail=f"请上传txt格式的文件,当前文件格式: {filename.split('.')[-1] if '.' in filename else '未知'}"
+ )
+
+ # 检查文件大小
+ file_size = await file.read(1) # 读取1字节获取文件信息
+ await file.seek(0) # 重置文件指针
+ if not file_size: # 文件为空
+ raise HTTPException(
+ status_code=400,
+ detail="上传的文件为空,请提供有效的敏感词文件"
+ )
+
+ conn = None
+ cursor = None
+ try:
+ # 读取文件内容
+ contents = await file.read()
+ # 按行分割内容,处理不同操作系统的换行符
+ lines = contents.decode("utf-8", errors="replace").splitlines()
+
+ # 过滤空行和仅含空白字符的行
+ sensitive_words = [line.strip() for line in lines if line.strip()]
+
+ if not sensitive_words:
+ return APIResponse(
+ code=200,
+ message="文件中没有有效的敏感词",
+ data={"imported": 0, "total": 0}
+ )
+
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 先查询数据库中已存在的敏感词
+ query = "SELECT name FROM sensitives WHERE name IN (%s)"
+ # 处理参数,根据敏感词数量生成占位符
+ placeholders = ', '.join(['%s'] * len(sensitive_words))
+ cursor.execute(query % placeholders, sensitive_words)
+ existing_words = {row['name'] for row in cursor.fetchall()}
+
+ # 过滤掉已存在的敏感词
+ new_words = [word for word in sensitive_words if word not in existing_words]
+
+ if not new_words:
+ return APIResponse(
+ code=200,
+ message="所有敏感词均已存在于数据库中",
+ data={
+ "total": len(sensitive_words),
+ "imported": 0,
+ "duplicates": len(sensitive_words),
+ "message": f"共处理{len(sensitive_words)}个敏感词,全部已存在,未导入任何新敏感词"
+ }
+ )
+
+ # 批量插入新的敏感词
+ insert_query = """
+ INSERT INTO sensitives (name, created_at, updated_at)
+ VALUES (%s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ """
+
+ # 准备参数列表
+ params = [(word,) for word in new_words]
+
+ # 执行批量插入
+ cursor.executemany(insert_query, params)
+ conn.commit()
+
+ # 重新加载最新的敏感词
+ set_forbidden_words(get_all_sensitive_words())
+
+ return APIResponse(
+ code=200,
+ message=f"敏感词批量导入成功",
+ data={
+ "total": len(sensitive_words),
+ "imported": len(new_words),
+ "duplicates": len(sensitive_words) - len(new_words),
+ "message": f"共处理{len(sensitive_words)}个敏感词,成功导入{len(new_words)}个,{len(sensitive_words) - len(new_words)}个已存在"
+ }
+ )
+
+ except UnicodeDecodeError:
+ raise HTTPException(
+ status_code=400,
+ detail="文件编码格式错误,请使用UTF-8编码的txt文件"
+ )
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise HTTPException(
+ status_code=500,
+ detail=f"批量导入敏感词失败: {str(e)}"
+ ) from e
+ except Exception as e:
+ raise HTTPException(
+ status_code=500,
+ detail=f"处理文件时发生错误: {str(e)}"
+ ) from e
+ finally:
+ await file.close()
+ db.close_connection(conn, cursor)
diff --git a/router/user_router.py b/router/user_router.py
new file mode 100644
index 0000000..af1fddb
--- /dev/null
+++ b/router/user_router.py
@@ -0,0 +1,246 @@
+from datetime import timedelta
+from typing import Optional
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from encryption.encrypt_decorator import encrypt_response
+from middle.auth_middleware import (
+ get_password_hash,
+ verify_password,
+ create_access_token,
+ ACCESS_TOKEN_EXPIRE_MINUTES,
+ get_current_user
+)
+from schema.response_schema import APIResponse
+from schema.user_schema import UserRegisterRequest, UserLoginRequest, UserResponse
+
+router = APIRouter(
+ prefix="/api/users",
+ tags=["用户管理"]
+)
+
+
+# 用户注册接口
+@router.post("/register", response_model=APIResponse, summary="用户注册")
+@encrypt_response()
+async def user_register(request: UserRegisterRequest):
+ """
+ 用户注册:
+ - 校验用户名是否已存在
+ - 加密密码后插入数据库
+ - 返回注册成功信息
+ """
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 1. 检查用户名是否已存在(唯一索引)
+ check_query = "SELECT username FROM users WHERE username = %s"
+ cursor.execute(check_query, (request.username,))
+ existing_user = cursor.fetchone()
+ if existing_user:
+ raise HTTPException(
+ status_code=400,
+ detail=f"用户名 '{request.username}' 已存在、请更换其他用户名"
+ )
+
+ # 2. 加密密码
+ hashed_password = get_password_hash(request.password)
+
+ # 3. 插入新用户到数据库
+ insert_query = """
+ INSERT INTO users (username, password)
+ VALUES (%s, %s)
+ """
+ cursor.execute(insert_query, (request.username, hashed_password))
+ conn.commit() # 提交事务
+
+ # 4. 返回注册成功响应
+ return APIResponse(
+ code=200, # 200 表示资源创建成功
+ message=f"用户 '{request.username}' 注册成功",
+ data=None
+ )
+ except MySQLError as e:
+ conn.rollback() # 数据库错误时回滚事务
+ raise Exception(f"注册失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 用户登录接口
+@router.post("/login", response_model=APIResponse, summary="用户登录(获取 Token)")
+@encrypt_response()
+async def user_login(request: UserLoginRequest):
+ """
+ 用户登录:
+ - 校验用户名是否存在
+ - 校验密码是否正确
+ - 生成 JWT Token 并返回
+ """
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 修复: SQL查询添加 created_at 和 updated_at 字段
+ query = """
+ SELECT id, username, password, created_at, updated_at
+ FROM users
+ WHERE username = %s
+ """
+ cursor.execute(query, (request.username,))
+ user = cursor.fetchone()
+
+ # 2. 校验用户名和密码
+ if not user or not verify_password(request.password, user["password"]):
+ raise HTTPException(
+ status_code=401,
+ detail="用户名或密码错误",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ # 3. 生成 Token(过期时间从配置读取)
+ access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ access_token = create_access_token(
+ data={"sub": user["username"]},
+ expires_delta=access_token_expires
+ )
+
+ # 4. 返回 Token 和用户基本信息
+ return APIResponse(
+ code=200,
+ message="登录成功",
+ data={
+ "access_token": access_token,
+ "token_type": "bearer",
+ "user": UserResponse(
+ id=user["id"],
+ username=user["username"],
+ created_at=user.get("created_at"),
+ updated_at=user.get("updated_at")
+ )
+ }
+ )
+ except MySQLError as e:
+ raise Exception(f"登录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 获取当前登录用户信息(需认证)
+@router.get("/me", response_model=APIResponse, summary="获取当前用户信息")
+@encrypt_response()
+async def get_current_user_info(
+ current_user: UserResponse = Depends(get_current_user) # 依赖认证中间件
+):
+ """
+ 获取当前登录用户信息:
+ - 需在请求头携带 Token(格式: Bearer )
+ - 认证通过后返回用户信息
+ """
+ return APIResponse(
+ code=200,
+ message="获取用户信息成功",
+ data=current_user
+ )
+
+
+# 获取用户列表(仅需登录权限)
+@router.get("/list", response_model=APIResponse, summary="获取用户列表")
+@encrypt_response()
+async def get_user_list(
+ page: int = Query(1, ge=1, description="页码、从1开始"),
+ page_size: int = Query(10, ge=1, le=100, description="每页条数、1-100之间"),
+ username: Optional[str] = Query(None, description="用户名模糊搜索"),
+ current_user: UserResponse = Depends(get_current_user) # 仅需登录即可访问(移除管理员校验)
+):
+ """
+ 获取用户列表:
+ - 需登录权限(请求头携带 Token: Bearer )
+ - 支持分页查询(page=页码、page_size=每页条数)
+ - 支持用户名模糊搜索(如输入"test"可匹配"test123"、"admin_test"等)
+ - 仅返回用户ID、用户名、创建时间、更新时间(不包含密码等敏感信息)
+ """
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 计算分页偏移量(page从1开始、偏移量=(页码-1)*每页条数)
+ offset = (page - 1) * page_size
+
+ # 基础查询(仅查非敏感字段)
+ base_query = """
+ SELECT id, username, created_at, updated_at
+ FROM users
+ """
+ # 总条数查询(用于分页计算)
+ count_query = "SELECT COUNT(*) as total FROM users"
+
+ # 条件拼接(支持用户名模糊搜索)
+ conditions = []
+ params = []
+ if username:
+ conditions.append("username LIKE %s")
+ params.append(f"%{username}%") # 模糊匹配:%表示任意字符
+
+ # 构建最终查询语句
+ if conditions:
+ where_clause = " WHERE " + " AND ".join(conditions)
+ final_query = f"{base_query}{where_clause} LIMIT %s OFFSET %s"
+ final_count_query = f"{count_query}{where_clause}"
+ params.extend([page_size, offset]) # 追加分页参数
+ else:
+ final_query = f"{base_query} LIMIT %s OFFSET %s"
+ final_count_query = count_query
+ params = [page_size, offset]
+
+ # 1. 查询用户列表数据
+ cursor.execute(final_query, params)
+ users = cursor.fetchall()
+
+ # 2. 查询总条数(用于计算总页数)
+ count_params = [f"%{username}%"] if username else []
+ cursor.execute(final_count_query, count_params)
+ total = cursor.fetchone()["total"]
+
+ # 3. 转换为UserResponse模型(确保字段匹配)
+ user_list = [
+ UserResponse(
+ id=user["id"],
+ username=user["username"],
+ created_at=user["created_at"],
+ updated_at=user["updated_at"]
+ )
+ for user in users
+ ]
+
+ # 4. 计算总页数(向上取整、如11条数据每页10条=2页)
+ total_pages = (total + page_size - 1) // page_size
+
+ # 返回结果(包含列表和分页信息)
+ return APIResponse(
+ code=200,
+ message="获取用户列表成功",
+ data={
+ "users": user_list,
+ "pagination": {
+ "page": page, # 当前页码
+ "page_size": page_size, # 每页条数
+ "total": total, # 总数据量
+ "total_pages": total_pages # 总页数
+ }
+ }
+ )
+ except MySQLError as e:
+ raise Exception(f"获取用户列表失败: {str(e)}") from e
+ finally:
+ # 无论成功失败、都关闭数据库连接
+ db.close_connection(conn, cursor)
diff --git a/schema/__pycache__/device_action_schema.cpython-310.pyc b/schema/__pycache__/device_action_schema.cpython-310.pyc
new file mode 100644
index 0000000..afe26b9
Binary files /dev/null and b/schema/__pycache__/device_action_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/device_danger_schema.cpython-310.pyc b/schema/__pycache__/device_danger_schema.cpython-310.pyc
new file mode 100644
index 0000000..ae1d3ad
Binary files /dev/null and b/schema/__pycache__/device_danger_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/device_schema.cpython-310.pyc b/schema/__pycache__/device_schema.cpython-310.pyc
new file mode 100644
index 0000000..3b1f01d
Binary files /dev/null and b/schema/__pycache__/device_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/face_schema.cpython-310.pyc b/schema/__pycache__/face_schema.cpython-310.pyc
new file mode 100644
index 0000000..07f39ae
Binary files /dev/null and b/schema/__pycache__/face_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/model_schema.cpython-310.pyc b/schema/__pycache__/model_schema.cpython-310.pyc
new file mode 100644
index 0000000..52aa5e3
Binary files /dev/null and b/schema/__pycache__/model_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/response_schema.cpython-310.pyc b/schema/__pycache__/response_schema.cpython-310.pyc
new file mode 100644
index 0000000..446e506
Binary files /dev/null and b/schema/__pycache__/response_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/sensitive_schema.cpython-310.pyc b/schema/__pycache__/sensitive_schema.cpython-310.pyc
new file mode 100644
index 0000000..843d0df
Binary files /dev/null and b/schema/__pycache__/sensitive_schema.cpython-310.pyc differ
diff --git a/schema/__pycache__/user_schema.cpython-310.pyc b/schema/__pycache__/user_schema.cpython-310.pyc
new file mode 100644
index 0000000..57fc400
Binary files /dev/null and b/schema/__pycache__/user_schema.cpython-310.pyc differ
diff --git a/schema/device_action_schema.py b/schema/device_action_schema.py
new file mode 100644
index 0000000..5f5f306
--- /dev/null
+++ b/schema/device_action_schema.py
@@ -0,0 +1,36 @@
+from datetime import datetime
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+# ------------------------------
+# 请求模型
+# ------------------------------
+class DeviceActionCreate(BaseModel):
+ """设备操作记录创建模型(0=离线、1=上线)"""
+ client_ip: str = Field(..., description="客户端IP")
+ action: int = Field(..., ge=0, le=1, description="操作状态(0=离线、1=上线)")
+
+
+# ------------------------------
+# 响应模型(单条记录)
+# ------------------------------
+class DeviceActionResponse(BaseModel):
+ """设备操作记录响应模型(与自增表对齐)"""
+ id: int = Field(..., description="自增主键ID")
+ client_ip: Optional[str] = Field(None, description="客户端IP")
+ action: Optional[int] = Field(None, description="操作状态(0=离线、1=上线)")
+ created_at: datetime = Field(..., description="记录创建时间")
+ updated_at: datetime = Field(..., description="记录更新时间")
+
+ # 支持从数据库结果直接转换
+ model_config = {"from_attributes": True}
+
+
+# ------------------------------
+# 列表响应模型(仅含 total + device_actions)
+# ------------------------------
+class DeviceActionListResponse(BaseModel):
+ """设备操作记录列表(仅核心返回字段)"""
+ total: int = Field(..., description="总记录数")
+ device_actions: List[DeviceActionResponse] = Field(..., description="操作记录列表")
\ No newline at end of file
diff --git a/schema/device_danger_schema.py b/schema/device_danger_schema.py
new file mode 100644
index 0000000..9ff55b9
--- /dev/null
+++ b/schema/device_danger_schema.py
@@ -0,0 +1,33 @@
+from datetime import datetime
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+# ------------------------------
+# 请求模型
+# ------------------------------
+class DeviceDangerCreateRequest(BaseModel):
+ """设备危险记录创建请求模型"""
+ client_ip: str = Field(..., max_length=100, description="设备IP地址(必须与devices表中IP对应)")
+ type: str = Field(..., max_length=255, description="危险类型(如:病毒检测、端口异常、权限泄露等)")
+ result: str = Field(..., description="危险检测结果/处理结果(如:检测到木马病毒、已隔离;端口22异常开放、已关闭)")
+
+
+# ------------------------------
+# 响应模型
+# ------------------------------
+class DeviceDangerResponse(BaseModel):
+ """单条设备危险记录响应模型(与device_danger表字段对齐、updated_at允许为null)"""
+ id: int = Field(..., description="危险记录主键ID")
+ client_ip: str = Field(..., max_length=100, description="设备IP地址")
+ type: str = Field(..., max_length=255, description="危险类型")
+ result: str = Field(..., description="危险检测结果/处理结果")
+ created_at: datetime = Field(..., description="记录创建时间(危险发生/检测时间)")
+ updated_at: Optional[datetime] = Field(None, description="记录更新时间(数据库中该字段当前为null)")
+ model_config = {"from_attributes": True}
+
+
+class DeviceDangerListResponse(BaseModel):
+ """设备危险记录列表响应模型(带分页)"""
+ total: int = Field(..., description="危险记录总数")
+ dangers: List[DeviceDangerResponse] = Field(..., description="设备危险记录列表")
\ No newline at end of file
diff --git a/schema/device_schema.py b/schema/device_schema.py
new file mode 100644
index 0000000..496dea5
--- /dev/null
+++ b/schema/device_schema.py
@@ -0,0 +1,56 @@
+from datetime import datetime
+from typing import Optional, List, Dict
+
+from pydantic import BaseModel, Field
+
+
+# ------------------------------
+# 请求模型
+# ------------------------------
+class DeviceCreateRequest(BaseModel):
+ """设备创建请求模型"""
+ ip: Optional[str] = Field(..., max_length=100, description="设备IP地址")
+ hostname: Optional[str] = Field(None, max_length=100, description="设备别名")
+ params: Optional[Dict] = Field(None, description="设备扩展参数(JSON格式)")
+
+
+# ------------------------------
+# 响应模型
+# ------------------------------
+class DeviceResponse(BaseModel):
+ """单设备信息响应模型(与数据库表字段对齐)"""
+ id: int = Field(..., description="设备主键ID")
+ device_id: Optional[int] = Field(None, description="关联设备ID(若历史记录IP无对应设备则为None)")
+ hostname: Optional[str] = Field(None, max_length=100, description="设备别名")
+ device_online_status: int = Field(..., description="在线状态(1-在线、0-离线)")
+ device_type: Optional[str] = Field(None, description="设备类型")
+ alarm_count: int = Field(..., description="报警次数")
+ params: Optional[str] = Field(None, description="扩展参数(JSON字符串)")
+ client_ip: Optional[str] = Field(None, max_length=100, description="设备IP地址")
+ is_need_handler: int = Field(..., description="是否需要处理(1-需要、0-不需要)")
+ created_at: datetime = Field(..., description="记录创建时间")
+ updated_at: datetime = Field(..., description="记录更新时间")
+ model_config = {"from_attributes": True} # 支持从数据库结果直接转换
+
+
+class DeviceListResponse(BaseModel):
+ """设备列表响应模型"""
+ total: int = Field(..., description="设备总数")
+ devices: List[DeviceResponse] = Field(..., description="设备列表")
+
+
+class DeviceStatusHistoryResponse(BaseModel):
+ """设备上下线记录响应模型"""
+ id: int = Field(..., description="记录ID")
+ device_id: int = Field(..., description="关联设备ID")
+ client_ip: Optional[str] = Field(None, description="设备IP地址")
+ status: int = Field(..., description="状态(1-在线、0-离线)")
+ status_time: datetime = Field(..., description="状态变更时间")
+
+ model_config = {"from_attributes": True}
+
+
+class DeviceStatusHistoryListResponse(BaseModel):
+ """设备上下线记录列表响应模型"""
+ total: int = Field(..., description="记录总数")
+ history: List[DeviceStatusHistoryResponse] = Field(..., description="上下线记录列表")
\ No newline at end of file
diff --git a/schema/face_schema.py b/schema/face_schema.py
new file mode 100644
index 0000000..9c4c0e4
--- /dev/null
+++ b/schema/face_schema.py
@@ -0,0 +1,41 @@
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import List, Optional
+
+
+# ------------------------------
+# 请求模型(前端传参校验)- 保留update的eigenvalue(如需更新特征值)
+# ------------------------------
+class FaceCreateRequest(BaseModel):
+ """创建人脸记录请求模型(无需ID、由数据库自增)"""
+ name: Optional[str] = Field(None, max_length=255, description="名称(可选、最长255字符)")
+
+
+class FaceUpdateRequest(BaseModel):
+ """更新人脸记录请求模型 - 保留eigenvalue(如需更新特征值、不影响返回)"""
+ name: Optional[str] = Field(None, max_length=255, description="名称(可选)")
+ eigenvalue: Optional[str] = Field(None, description="特征值(可选、文件处理后可更新)") # 保留更新能力
+ address: Optional[str] = Field(None, description="图片完整路径(可选、更新图片时使用)")
+
+
+# ------------------------------
+# 响应模型(后端返回数据)- 核心修改:删除eigenvalue字段
+# ------------------------------
+class FaceResponse(BaseModel):
+ """人脸记录响应模型(仅返回需要的字段、移除eigenvalue)"""
+ id: int = Field(..., description="主键ID(数据库自增)")
+ name: Optional[str] = Field(None, description="名称")
+ address: Optional[str] = Field(None, description="人脸图片完整保存路径(数据库新增字段)") # 仅保留address
+ created_at: datetime = Field(..., description="记录创建时间(数据库自动生成)")
+ updated_at: datetime = Field(..., description="记录更新时间(数据库自动生成)")
+
+ # 关键配置:支持从数据库查询结果(字典)直接转换
+ model_config = {"from_attributes": True}
+
+
+class FaceListResponse(BaseModel):
+ """人脸列表分页响应模型(结构不变、内部FaceResponse已移除eigenvalue)"""
+ total: int = Field(..., description="筛选后的总记录数")
+ faces: List[FaceResponse] = Field(..., description="当前页的人脸记录列表")
+
+ model_config = {"from_attributes": True}
\ No newline at end of file
diff --git a/schema/model_schema.py b/schema/model_schema.py
new file mode 100644
index 0000000..2ba5f08
--- /dev/null
+++ b/schema/model_schema.py
@@ -0,0 +1,37 @@
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import List, Optional
+
+
+# 请求模型
+class ModelCreateRequest(BaseModel):
+ name: str = Field(..., max_length=255, description="模型名称(必填、如:yolo-v8s-car)")
+ description: Optional[str] = Field(None, description="模型描述(可选)")
+ is_default: Optional[bool] = Field(False, description="是否设为默认模型")
+
+
+class ModelUpdateRequest(BaseModel):
+ name: Optional[str] = Field(None, max_length=255, description="模型名称(可选修改)")
+ description: Optional[str] = Field(None, description="模型描述(可选修改)")
+ is_default: Optional[bool] = Field(None, description="是否设为默认模型(可选切换)")
+
+
+# 响应模型
+class ModelResponse(BaseModel):
+ id: int = Field(..., description="模型ID")
+ name: str = Field(..., description="模型名称")
+ path: str = Field(..., description="模型文件相对路径")
+ is_default: bool = Field(..., description="是否默认模型")
+ description: Optional[str] = Field(None, description="模型描述")
+ file_size: Optional[int] = Field(None, description="文件大小(字节)")
+ created_at: datetime = Field(..., description="创建时间")
+ updated_at: datetime = Field(..., description="更新时间")
+
+ model_config = {"from_attributes": True}
+
+
+class ModelListResponse(BaseModel):
+ total: int = Field(..., description="总记录数")
+ models: List[ModelResponse] = Field(..., description="当前页模型列表")
+
+ model_config = {"from_attributes": True}
diff --git a/schema/response_schema.py b/schema/response_schema.py
new file mode 100644
index 0000000..aa5a7d4
--- /dev/null
+++ b/schema/response_schema.py
@@ -0,0 +1,13 @@
+from typing import Optional, Any
+
+from pydantic import BaseModel, Field
+
+
+class APIResponse(BaseModel):
+ """统一 API 响应模型(所有接口必返此格式)"""
+ code: int = Field(..., description="状态码: 200=成功、4xx=客户端错误、5xx=服务端错误")
+ message: str = Field(..., description="响应信息: 成功/错误描述")
+ data: Optional[Any] = Field(None, description="响应数据: 成功时返回、错误时为 None")
+
+ # Pydantic V2 配置(支持从 ORM 对象转换)
+ model_config = {"from_attributes": True}
diff --git a/schema/sensitive_schema.py b/schema/sensitive_schema.py
new file mode 100644
index 0000000..656159e
--- /dev/null
+++ b/schema/sensitive_schema.py
@@ -0,0 +1,38 @@
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import List, Optional
+
+
+# ------------------------------
+# 请求模型(前端传参校验)
+# ------------------------------
+class SensitiveCreateRequest(BaseModel):
+ """创建敏感信息记录请求模型"""
+ name: str = Field(..., max_length=255, description="敏感词内容(必填)")
+
+
+class SensitiveUpdateRequest(BaseModel):
+ """更新敏感信息记录请求模型"""
+ name: Optional[str] = Field(None, max_length=255, description="敏感词内容(可选修改)")
+
+
+# ------------------------------
+# 响应模型(后端返回数据)
+# ------------------------------
+class SensitiveResponse(BaseModel):
+ """敏感信息单条记录响应模型"""
+ id: int = Field(..., description="主键ID")
+ name: str = Field(..., description="敏感词内容")
+ created_at: datetime = Field(..., description="记录创建时间")
+ updated_at: datetime = Field(..., description="记录更新时间")
+
+ # 支持从数据库查询结果(字典/对象)自动转换
+ model_config = {"from_attributes": True}
+
+
+class SensitiveListResponse(BaseModel):
+ """敏感信息分页列表响应模型(新增)"""
+ total: int = Field(..., description="敏感词总记录数")
+ sensitives: List[SensitiveResponse] = Field(..., description="当前页敏感词列表")
+
+ model_config = {"from_attributes": True}
\ No newline at end of file
diff --git a/schema/user_schema.py b/schema/user_schema.py
new file mode 100644
index 0000000..30a86cc
--- /dev/null
+++ b/schema/user_schema.py
@@ -0,0 +1,40 @@
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import List, Optional
+
+
+# ------------------------------
+# 请求模型(前端传参校验)
+# ------------------------------
+class UserRegisterRequest(BaseModel):
+ """用户注册请求模型"""
+ username: str = Field(..., min_length=3, max_length=50, description="用户名(3-50字符)")
+ password: str = Field(..., min_length=6, max_length=100, description="密码(6-100字符)")
+
+
+class UserLoginRequest(BaseModel):
+ """用户登录请求模型"""
+ username: str = Field(..., description="用户名")
+ password: str = Field(..., description="密码")
+
+
+# ------------------------------
+# 响应模型(后端返回用户数据)
+# ------------------------------
+class UserResponse(BaseModel):
+ """用户信息响应模型(隐藏密码等敏感字段)"""
+ id: int = Field(..., description="用户ID")
+ username: str = Field(..., description="用户名")
+ created_at: datetime = Field(..., description="创建时间")
+ updated_at: datetime = Field(..., description="更新时间")
+
+ # Pydantic V2 配置(支持从数据库查询结果转换)
+ model_config = {"from_attributes": True}
+
+
+class UserListResponse(BaseModel):
+ """用户列表分页响应模型(与设备/人脸列表结构对齐)"""
+ total: int = Field(..., description="用户总数")
+ users: List[UserResponse] = Field(..., description="当前页用户列表")
+
+ model_config = {"from_attributes": True}
\ No newline at end of file
diff --git a/service/__pycache__/device_action_service.cpython-310.pyc b/service/__pycache__/device_action_service.cpython-310.pyc
new file mode 100644
index 0000000..0a69243
Binary files /dev/null and b/service/__pycache__/device_action_service.cpython-310.pyc differ
diff --git a/service/__pycache__/device_danger_service.cpython-310.pyc b/service/__pycache__/device_danger_service.cpython-310.pyc
new file mode 100644
index 0000000..ac99558
Binary files /dev/null and b/service/__pycache__/device_danger_service.cpython-310.pyc differ
diff --git a/service/__pycache__/device_service.cpython-310.pyc b/service/__pycache__/device_service.cpython-310.pyc
new file mode 100644
index 0000000..d966dbf
Binary files /dev/null and b/service/__pycache__/device_service.cpython-310.pyc differ
diff --git a/service/__pycache__/face_service.cpython-310.pyc b/service/__pycache__/face_service.cpython-310.pyc
new file mode 100644
index 0000000..3da7a8e
Binary files /dev/null and b/service/__pycache__/face_service.cpython-310.pyc differ
diff --git a/service/__pycache__/file_service.cpython-310.pyc b/service/__pycache__/file_service.cpython-310.pyc
new file mode 100644
index 0000000..f9fdcdb
Binary files /dev/null and b/service/__pycache__/file_service.cpython-310.pyc differ
diff --git a/service/__pycache__/model_service.cpython-310.pyc b/service/__pycache__/model_service.cpython-310.pyc
new file mode 100644
index 0000000..f997b68
Binary files /dev/null and b/service/__pycache__/model_service.cpython-310.pyc differ
diff --git a/service/__pycache__/ocr_service.cpython-310.pyc b/service/__pycache__/ocr_service.cpython-310.pyc
new file mode 100644
index 0000000..95334fd
Binary files /dev/null and b/service/__pycache__/ocr_service.cpython-310.pyc differ
diff --git a/service/__pycache__/sensitive_service.cpython-310.pyc b/service/__pycache__/sensitive_service.cpython-310.pyc
new file mode 100644
index 0000000..11cd261
Binary files /dev/null and b/service/__pycache__/sensitive_service.cpython-310.pyc differ
diff --git a/service/device_action_service.py b/service/device_action_service.py
new file mode 100644
index 0000000..a3c49c6
--- /dev/null
+++ b/service/device_action_service.py
@@ -0,0 +1,43 @@
+from ds.db import db
+from schema.device_action_schema import DeviceActionCreate, DeviceActionResponse
+from mysql.connector import Error as MySQLError
+
+
+# 新增设备操作记录
+def add_device_action(client_ip: str, action: int) -> DeviceActionResponse:
+ """
+ 新增设备操作记录(内部方法、非接口)
+ :param action_data: 含client_ip和action(0/1)
+ :return: 新增的完整记录
+ """
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 插入SQL(id自增、依赖数据库自动生成)
+ insert_query = """
+ INSERT INTO device_action
+ (client_ip, action, created_at, updated_at)
+ VALUES (%s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ """
+ cursor.execute(insert_query, (
+ client_ip,
+ action
+ ))
+ conn.commit()
+
+ # 获取新增记录(通过自增ID查询)
+ new_id = cursor.lastrowid
+ cursor.execute("SELECT * FROM device_action WHERE id = %s", (new_id,))
+ new_action = cursor.fetchone()
+
+ return DeviceActionResponse(**new_action)
+
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise Exception(f"新增记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
diff --git a/service/device_danger_service.py b/service/device_danger_service.py
new file mode 100644
index 0000000..c00b91e
--- /dev/null
+++ b/service/device_danger_service.py
@@ -0,0 +1,78 @@
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from schema.device_danger_schema import DeviceDangerCreateRequest, DeviceDangerResponse
+
+
+# ------------------------------
+# 内部工具方法 - 检查设备是否存在(复用设备表逻辑)
+# ------------------------------
+def check_device_exist(client_ip: str) -> bool:
+ """
+ 检查指定IP的设备是否在devices表中存在
+
+ :param client_ip: 设备IP地址
+ :return: 存在返回True、不存在返回False
+ """
+ if not client_ip:
+ raise ValueError("设备IP不能为空")
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ cursor.execute("SELECT id FROM devices WHERE client_ip = %s", (client_ip,))
+ return cursor.fetchone() is not None
+ except MySQLError as e:
+ raise Exception(f"检查设备存在性失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# ------------------------------
+# 内部工具方法 - 创建设备危险记录(核心插入逻辑)
+# ------------------------------
+def create_danger_record(danger_data: DeviceDangerCreateRequest) -> DeviceDangerResponse:
+ """
+ 内部工具方法:向device_danger表插入新的危险记录
+
+ :param danger_data: 危险记录创建请求数据
+ :return: 创建成功的危险记录模型对象
+ """
+ # 先检查设备是否存在
+ if not check_device_exist(danger_data.client_ip):
+ raise ValueError(f"IP为 {danger_data.client_ip} 的设备不存在、无法创建危险记录")
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 插入危险记录(id自增、时间自动填充)
+ insert_query = """
+ INSERT INTO device_danger
+ (client_ip, type, result, created_at, updated_at)
+ VALUES (%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
+ """
+ cursor.execute(insert_query, (
+ danger_data.client_ip,
+ danger_data.type,
+ danger_data.result
+ ))
+ conn.commit()
+
+ # 获取刚创建的记录(用自增ID查询)
+ danger_id = cursor.lastrowid
+ cursor.execute("SELECT * FROM device_danger WHERE id = %s", (danger_id,))
+ new_danger = cursor.fetchone()
+
+ return DeviceDangerResponse(**new_danger)
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise Exception(f"插入危险记录失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
diff --git a/service/device_service.py b/service/device_service.py
new file mode 100644
index 0000000..7cae6cc
--- /dev/null
+++ b/service/device_service.py
@@ -0,0 +1,250 @@
+import threading
+import time
+from typing import Optional
+
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+from service.device_action_service import add_device_action
+_last_alarm_timestamps: dict[str, float] = {}
+_timestamp_lock = threading.Lock()
+
+# 获取所有去重的客户端IP列表
+def get_unique_client_ips() -> list[str]:
+ """获取所有去重的客户端IP列表"""
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ query = "SELECT DISTINCT client_ip FROM devices WHERE client_ip IS NOT NULL"
+ cursor.execute(query)
+ results = cursor.fetchall()
+ return [item['client_ip'] for item in results]
+
+ except MySQLError as e:
+ raise Exception(f"获取客户端IP列表失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+# 通过客户端IP更新设备是否需要处理
+def update_is_need_handler_by_client_ip(client_ip: str, is_need_handler: int) -> bool:
+ """
+ 通过客户端IP更新设备的「是否需要处理」状态(is_need_handler字段)
+ """
+ # 参数合法性校验
+ if not client_ip:
+ raise ValueError("客户端IP不能为空")
+
+ # 校验is_need_handler取值(需与数据库字段类型匹配、通常为0/1 tinyint)
+ if is_need_handler not in (0, 1):
+ raise ValueError("是否需要处理(is_need_handler)必须是0(不需要)或1(需要)")
+
+ conn = None
+ cursor = None
+ try:
+ # 2. 获取数据库连接与游标
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 3. 先校验设备是否存在(通过client_ip定位)
+ cursor.execute(
+ "SELECT id FROM devices WHERE client_ip = %s",
+ (client_ip,)
+ )
+ device = cursor.fetchone()
+ if not device:
+ raise ValueError(f"客户端IP为 {client_ip} 的设备不存在、无法更新「是否需要处理」状态")
+
+ # 4. 执行更新操作(同时更新时间戳、保持与其他更新逻辑一致性)
+ update_query = """
+ UPDATE devices
+ SET is_need_handler = %s,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE client_ip = %s
+ """
+ cursor.execute(update_query, (is_need_handler, client_ip))
+
+ # 5. 确认更新生效(判断影响行数、避免无意义更新)
+ if cursor.rowcount <= 0:
+ raise Exception(f"更新失败:客户端IP {client_ip} 的设备未发生状态变更(可能已为目标值)")
+
+ # 6. 提交事务
+ conn.commit()
+ return True
+
+ except MySQLError as e:
+ # 数据库异常时回滚事务
+ if conn:
+ conn.rollback()
+ raise Exception(f"数据库操作失败:更新设备「是否需要处理」状态时出错 - {str(e)}") from e
+ finally:
+ # 无论成功失败、都关闭数据库连接(避免连接泄漏)
+ db.close_connection(conn, cursor)
+
+def increment_alarm_count_by_ip(client_ip: str) -> bool:
+ """
+ 通过客户端IP增加设备的报警次数,相同IP 200ms内重复调用会被忽略
+
+ :param client_ip: 客户端IP地址
+ :return: 操作是否成功(是否实际执行了数据库更新)
+ """
+ if not client_ip:
+ raise ValueError("客户端IP不能为空")
+
+ current_time = time.time() # 获取当前时间戳(秒,含小数)
+ with _timestamp_lock: # 确保线程安全的字典操作
+ last_time: Optional[float] = _last_alarm_timestamps.get(client_ip)
+
+ # 如果存在最近记录且间隔小于200ms,直接返回False(不执行更新)
+ if last_time is not None and (current_time - last_time) < 0.2:
+ return False
+
+ # 更新当前IP的最近调用时间
+ _last_alarm_timestamps[client_ip] = current_time
+
+ # 2. 执行数据库更新操作(只有通过时间校验才会执行)
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 检查设备是否存在
+ cursor.execute("SELECT id FROM devices WHERE client_ip = %s", (client_ip,))
+ device = cursor.fetchone()
+ if not device:
+ raise ValueError(f"客户端IP为 {client_ip} 的设备不存在")
+
+ # 报警次数加1、并更新时间戳
+ update_query = """
+ UPDATE devices
+ SET alarm_count = alarm_count + 1,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE client_ip = %s
+ """
+ cursor.execute(update_query, (client_ip,))
+ conn.commit()
+
+ return True
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise Exception(f"更新报警次数失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+# 通过客户端IP更新设备在线状态
+def update_online_status_by_ip(client_ip: str, online_status: int) -> bool:
+ """
+ 通过客户端IP更新设备的在线状态
+
+ :param client_ip: 客户端IP地址
+ :param online_status: 在线状态(1-在线、0-离线)
+ :return: 操作是否成功
+ """
+ if not client_ip:
+ raise ValueError("客户端IP不能为空")
+
+ if online_status not in (0, 1):
+ raise ValueError("在线状态必须是0(离线)或1(在线)")
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 检查设备是否存在并获取设备ID
+ cursor.execute("SELECT id, device_online_status FROM devices WHERE client_ip = %s", (client_ip,))
+ device = cursor.fetchone()
+ if not device:
+ raise ValueError(f"客户端IP为 {client_ip} 的设备不存在")
+
+ # 状态无变化则不操作
+ if device['device_online_status'] == online_status:
+ return True
+
+ # 更新在线状态和时间戳
+ update_query = """
+ UPDATE devices
+ SET device_online_status = %s,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE client_ip = %s
+ """
+ cursor.execute(update_query, (online_status, client_ip))
+
+ # 记录状态变更历史
+ add_device_action(client_ip, online_status)
+
+ conn.commit()
+ return True
+ except MySQLError as e:
+ if conn:
+ conn.rollback()
+ raise Exception(f"更新设备在线状态失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+# 通过客户端IP查询设备在数据库中存在
+def is_device_exist_by_ip(client_ip: str) -> bool:
+ """
+ 通过客户端IP查询设备在数据库中是否存在
+ """
+ if not client_ip:
+ raise ValueError("客户端IP不能为空")
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 查询设备是否存在
+ cursor.execute(
+ "SELECT id FROM devices WHERE client_ip = %s",
+ (client_ip,)
+ )
+ device = cursor.fetchone()
+
+ # 如果查询到结果则存在,否则不存在
+ return bool(device)
+
+ except MySQLError as e:
+ raise Exception(f"查询设备是否存在失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+# 根据客户端IP获取是否需要处理
+def get_is_need_handler_by_ip(client_ip: str) -> int:
+ """
+ 通过客户端IP查询设备的is_need_handler状态
+
+ :param client_ip: 客户端IP地址
+ :return: 设备的is_need_handler状态(0-不需要处理,1-需要处理)
+ """
+ if not client_ip:
+ raise ValueError("客户端IP不能为空")
+
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 查询设备的is_need_handler状态
+ cursor.execute(
+ "SELECT is_need_handler FROM devices WHERE client_ip = %s",
+ (client_ip,)
+ )
+ device = cursor.fetchone()
+
+ if not device:
+ raise ValueError(f"客户端IP为 {client_ip} 的设备不存在")
+
+ return device['is_need_handler']
+
+ except MySQLError as e:
+ raise Exception(f"查询设备is_need_handler状态失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
diff --git a/service/face_service.py b/service/face_service.py
new file mode 100644
index 0000000..fc64f2a
--- /dev/null
+++ b/service/face_service.py
@@ -0,0 +1,339 @@
+import cv2
+from io import BytesIO
+from PIL import Image
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+
+import numpy as np
+import threading
+from insightface.app import FaceAnalysis
+
+# 全局变量定义
+_insightface_app = None
+_known_faces_embeddings = {} # 存储已知人脸特征 {姓名: 特征向量}
+_known_faces_names = [] # 存储已知人脸姓名列表
+
+
+def init_insightface():
+ """初始化InsightFace引擎"""
+ global _insightface_app
+ if _insightface_app is not None:
+ print("InsightFace引擎已初始化,无需重复执行")
+ return _insightface_app
+
+ try:
+ print("正在初始化 InsightFace 引擎(模型:buffalo_l)...")
+ # 初始化引擎,指定模型路径和计算 providers
+ app = FaceAnalysis(
+ name='buffalo_l',
+ root='~/.insightface',
+ providers=['CPUExecutionProvider'] # 如需GPU可添加'CUDAExecutionProvider'
+ )
+ app.prepare(ctx_id=0, det_size=(640, 640)) # 调整检测尺寸
+ print("InsightFace 引擎初始化完成")
+
+ # 初始化时加载人脸特征库
+ init_face_data()
+
+ _insightface_app = app
+ return app
+ except Exception as e:
+ print(f"InsightFace 初始化失败:{str(e)}")
+ _insightface_app = None
+ return None
+
+
+def init_face_data():
+ """初始化或更新人脸特征库(清空旧数据,避免重复)"""
+ global _known_faces_embeddings, _known_faces_names
+ # 清空原有数据,防止重复加载
+ _known_faces_embeddings.clear()
+ _known_faces_names.clear()
+
+ try:
+ face_data = get_all_face_name_with_eigenvalue() # 假设该函数已定义
+ print(f"已加载 {len(face_data)} 个人脸数据")
+ for person_name, eigenvalue_data in face_data.items():
+ # 解析特征值(支持numpy数组或字符串格式)
+ if isinstance(eigenvalue_data, np.ndarray):
+ eigenvalue = eigenvalue_data.astype(np.float32)
+ elif isinstance(eigenvalue_data, str):
+ # 增强字符串解析:支持逗号/空格分隔,清理特殊字符
+ cleaned = (eigenvalue_data
+ .replace("[", "").replace("]", "")
+ .replace("\n", "").replace(",", " ")
+ .strip())
+ values = [v for v in cleaned.split() if v] # 过滤空字符串
+ if not values:
+ print(f"特征值解析失败(空值),跳过 {person_name}")
+ continue
+ eigenvalue = np.array(list(map(float, values)), dtype=np.float32)
+ else:
+ print(f"不支持的特征值类型({type(eigenvalue_data)}),跳过 {person_name}")
+ continue
+
+ # 特征值归一化(确保相似度计算一致性)
+ norm = np.linalg.norm(eigenvalue)
+ if norm == 0:
+ print(f"特征值为零向量,跳过 {person_name}")
+ continue
+ eigenvalue = eigenvalue / norm
+
+ # 更新全局特征库
+ _known_faces_embeddings[person_name] = eigenvalue
+ _known_faces_names.append(person_name)
+
+ print(f"成功加载 {len(_known_faces_names)} 个人脸的特征库")
+ except Exception as e:
+ print(f"加载人脸特征库失败: {e}")
+
+
+def update_face_data():
+ """更新人脸特征库(清空旧数据,加载最新数据)"""
+ global _known_faces_embeddings, _known_faces_names
+
+ print("开始更新人脸特征库...")
+
+ # 清空原有数据
+ _known_faces_embeddings.clear()
+ _known_faces_names.clear()
+
+ try:
+ # 获取最新人脸数据
+ face_data = get_all_face_name_with_eigenvalue()
+ print(f"获取到 {len(face_data)} 条最新人脸数据")
+
+ # 处理并加载新数据(复用原有解析逻辑)
+ for person_name, eigenvalue_data in face_data.items():
+ # 解析特征值(支持numpy数组或字符串格式)
+ if isinstance(eigenvalue_data, np.ndarray):
+ eigenvalue = eigenvalue_data.astype(np.float32)
+ elif isinstance(eigenvalue_data, str):
+ # 增强字符串解析:支持逗号/空格分隔,清理特殊字符
+ cleaned = (eigenvalue_data
+ .replace("[", "").replace("]", "")
+ .replace("\n", "").replace(",", " ")
+ .strip())
+ values = [v for v in cleaned.split() if v] # 过滤空字符串
+ if not values:
+ print(f"特征值解析失败(空值),跳过 {person_name}")
+ continue
+ eigenvalue = np.array(list(map(float, values)), dtype=np.float32)
+ else:
+ print(f"不支持的特征值类型({type(eigenvalue_data)}),跳过 {person_name}")
+ continue
+
+ # 特征值归一化(确保相似度计算一致性)
+ norm = np.linalg.norm(eigenvalue)
+ if norm == 0:
+ print(f"特征值为零向量,跳过 {person_name}")
+ continue
+ eigenvalue = eigenvalue / norm
+
+ # 更新全局特征库
+ _known_faces_embeddings[person_name] = eigenvalue
+ _known_faces_names.append(person_name)
+
+ print(f"人脸特征库更新完成,共加载 {len(_known_faces_names)} 个人脸数据")
+ return True # 更新成功
+ except Exception as e:
+ print(f"人脸特征库更新失败: {e}")
+ return False # 更新失败
+
+
+def detect(frame, similarity_threshold=0.4):
+ global _insightface_app, _known_faces_embeddings
+
+ # 校验输入有效性
+ if frame is None or frame.size == 0:
+ return (False, "无效的输入帧数据")
+
+ # 校验引擎和特征库状态
+ if not _insightface_app:
+ return (False, "人脸引擎未初始化")
+ if not _known_faces_embeddings:
+ return (False, "人脸特征库为空")
+
+ try:
+ # 执行人脸检测与特征提取
+ faces = _insightface_app.get(frame)
+ except Exception as e:
+ return (False, f"检测错误: {str(e)}")
+
+ result_parts = []
+ has_matched_known_face = False # 是否有匹配到已知人脸
+
+ for face in faces:
+ # 归一化当前人脸特征
+ face_embedding = face.embedding.astype(np.float32)
+ norm = np.linalg.norm(face_embedding)
+ if norm == 0:
+ result_parts.append("检测到人脸但特征值为零向量(忽略)")
+ continue
+ face_embedding = face_embedding / norm
+
+ # 与已知特征库比对
+ max_similarity, best_match_name = -1.0, "Unknown"
+ for name, known_emb in _known_faces_embeddings.items():
+ similarity = np.dot(face_embedding, known_emb) # 余弦相似度
+ if similarity > max_similarity:
+ max_similarity = similarity
+ best_match_name = name
+
+ # 判断是否匹配成功
+ is_matched = max_similarity >= similarity_threshold
+
+ if is_matched:
+ has_matched_known_face = True
+
+ # 记录结果(边界框转为整数列表)
+ bbox = face.bbox.astype(int).tolist()
+ result_parts.append(
+ f"{'匹配' if is_matched else '未匹配'}: {best_match_name} "
+ f"(相似度: {max_similarity:.2f}, 边界框: {bbox})"
+ )
+
+ # 构建最终结果
+ result_str = "未检测到人脸" if not result_parts else "; ".join(result_parts)
+ return (has_matched_known_face, result_str)
+
+
+# 上传图片并提取特征
+def add_binary_data(binary_data):
+ """
+ 接收单张图片的二进制数据、提取特征并保存
+ 返回:(True, 特征值numpy数组) 或 (False, 错误信息字符串)
+ """
+ global _insightface_app, _feature_list
+
+ # 1. 先检查引擎是否初始化成功
+ if not _insightface_app:
+ init_result = init_insightface() # 尝试重新初始化
+ if not init_result:
+ error_msg = "InsightFace引擎未初始化、无法检测人脸"
+ print(error_msg)
+ return False, error_msg
+
+ try:
+ # 2. 验证二进制数据有效性
+ if len(binary_data) < 1024: # 过滤过小的无效图片(小于1KB)
+ error_msg = f"图片过小({len(binary_data)}字节)、可能不是有效图片"
+ print(error_msg)
+ return False, error_msg
+
+ # 3. 二进制数据转CV2格式(关键步骤、避免通道错误)
+ try:
+ img = Image.open(BytesIO(binary_data)).convert("RGB") # 强制转RGB
+ frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) # InsightFace需要BGR格式
+ except Exception as e:
+ error_msg = f"图片格式转换失败:{str(e)}"
+ print(error_msg)
+ return False, error_msg
+
+ # 4. 检查图片尺寸(避免极端尺寸导致检测失败)
+ height, width = frame.shape[:2]
+ if height < 64 or width < 64: # 人脸检测最小建议尺寸
+ error_msg = f"图片尺寸过小({width}x{height})、需至少64x64像素"
+ print(error_msg)
+ return False, error_msg
+
+ # 5. 调用InsightFace检测人脸
+ print(f"开始检测人脸(图片尺寸:{width}x{height}、格式:BGR)")
+ faces = _insightface_app.get(frame)
+
+ if not faces:
+ error_msg = "未检测到人脸(请确保图片包含清晰正面人脸、无遮挡、不模糊)"
+ print(error_msg)
+ return False, error_msg
+
+ # 6. 提取特征并保存
+ current_feature = faces[0].embedding
+ _feature_list.append(current_feature)
+ print(f"人脸检测成功、提取特征值(维度:{current_feature.shape[0]})、累计特征数:{len(_feature_list)}")
+ return True, current_feature
+
+ except Exception as e:
+ error_msg = f"处理图片时发生异常:{str(e)}"
+ print(error_msg)
+ return False, error_msg
+
+
+# 获取数据库最新的人脸及其特征
+def get_all_face_name_with_eigenvalue() -> dict:
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ query = "SELECT name, eigenvalue FROM face WHERE name IS NOT NULL"
+ cursor.execute(query)
+ faces = cursor.fetchall()
+
+ name_to_eigenvalues = {}
+ for face in faces:
+ name = face["name"]
+ eigenvalue = face["eigenvalue"]
+ if name in name_to_eigenvalues:
+ name_to_eigenvalues[name].append(eigenvalue)
+ else:
+ name_to_eigenvalues[name] = [eigenvalue]
+
+ face_dict = {}
+ for name, eigenvalues in name_to_eigenvalues.items():
+ if len(eigenvalues) > 1:
+ face_dict[name] = get_average_feature(eigenvalues)
+ else:
+ face_dict[name] = eigenvalues[0]
+
+ return face_dict
+
+ except MySQLError as e:
+ raise Exception(f"获取人脸特征失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+
+# 获取平均特征值
+def get_average_feature(features=None):
+ global _feature_list
+ try:
+ if features is None:
+ features = _feature_list
+ if not isinstance(features, list) or len(features) == 0:
+ print("输入必须是包含至少一个特征值的列表")
+ return None
+
+ processed_features = []
+ for i, embedding in enumerate(features):
+ try:
+ if isinstance(embedding, str):
+ embedding_str = embedding.replace('[', '').replace(']', '').replace(',', ' ').strip()
+ embedding_list = [float(num) for num in embedding_str.split() if num.strip()]
+ embedding_np = np.array(embedding_list, dtype=np.float32)
+ else:
+ embedding_np = np.array(embedding, dtype=np.float32)
+
+ if len(embedding_np.shape) == 1:
+ processed_features.append(embedding_np)
+ print(f"已添加第 {i + 1} 个特征值用于计算平均值")
+ else:
+ print(f"跳过第 {i + 1} 个特征值:不是一维数组")
+ except Exception as e:
+ print(f"处理第 {i + 1} 个特征值时出错:{str(e)}")
+
+ if not processed_features:
+ print("没有有效的特征值用于计算平均值")
+ return None
+
+ dims = {feat.shape[0] for feat in processed_features}
+ if len(dims) > 1:
+ print(f"特征值维度不一致:{dims}、无法计算平均值")
+ return None
+
+ avg_feature = np.mean(processed_features, axis=0)
+ print(f"计算成功:{len(processed_features)} 个特征值的平均向量(维度:{avg_feature.shape[0]})")
+ return avg_feature
+ except Exception as e:
+ print(f"计算平均特征值出错:{str(e)}")
+ return None
diff --git a/service/file_service.py b/service/file_service.py
new file mode 100644
index 0000000..42a0d6d
--- /dev/null
+++ b/service/file_service.py
@@ -0,0 +1,343 @@
+import os
+import re
+import shutil
+from datetime import datetime
+from PIL import ImageDraw, ImageFont
+from fastapi import UploadFile
+import cv2
+from PIL import Image
+import numpy as np
+
+# 上传根目录
+UPLOAD_ROOT = "upload"
+PRE = "/api/file/download/"
+
+# 确保上传根目录存在
+os.makedirs(UPLOAD_ROOT, exist_ok=True)
+
+
+
+def save_detect_file(client_ip: str, image_np: np.ndarray, file_type: str) -> str:
+ """保存numpy数组格式的PNG图片到detect目录,返回下载路径"""
+ today = datetime.now()
+ year = today.strftime("%Y")
+ month = today.strftime("%m")
+ day = today.strftime("%d")
+
+ # 构建目录路径: upload/detect/客户端IP/type/年/月/日(包含UPLOAD_ROOT)
+ file_dir = os.path.join(
+ UPLOAD_ROOT,
+ "detect",
+ client_ip,
+ file_type,
+ year,
+ month,
+ day
+ )
+
+ # 创建目录(确保目录存在)
+ os.makedirs(file_dir, exist_ok=True)
+
+ # 生成当前时间戳作为文件名,确保唯一性
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
+ filename = f"{timestamp}.png"
+
+ # 1. 完整路径:用于实际保存文件(包含UPLOAD_ROOT)
+ full_path = os.path.join(file_dir, filename)
+ # 2. 相对路径:用于返回给前端(移除UPLOAD_ROOT前缀)
+ relative_path = full_path.replace(UPLOAD_ROOT, "", 1).lstrip(os.sep)
+
+ # 保存numpy数组为PNG图片
+ try:
+ # -------- 新增/修改:处理颜色通道和数据类型 --------
+ # 1. 数据类型转换:确保是uint8(若为float32且范围0-1,需转成0-255的uint8)
+ if image_np.dtype != np.uint8:
+ image_np = (image_np * 255).astype(np.uint8)
+
+ # 2. 通道顺序转换:若为OpenCV的BGR格式,转成PIL需要的RGB格式
+ image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
+
+ # 3. 转换为PIL Image并保存
+ img = Image.fromarray(image_rgb)
+ img.save(full_path, format='PNG')
+ except Exception as e:
+ # 处理可能的异常(如数组格式不正确)
+ raise Exception(f"保存图片失败: {str(e)}")
+
+ # 统一路径分隔符为/,拼接前缀返回
+ return PRE + relative_path.replace(os.sep, "/")
+
+
+def save_detect_yolo_file(
+ client_ip: str,
+ image_np: np.ndarray,
+ detection_results: list,
+ file_type: str = "yolo"
+) -> str:
+
+
+ print("......................")
+ """
+ 保存YOLO检测结果图片(在原图上绘制边界框+标签),返回前端可访问的下载路径
+ """
+ # 输入参数验证
+ if not isinstance(image_np, np.ndarray):
+ raise ValueError(f"输入image_np必须是numpy数组,当前类型:{type(image_np)}")
+ if image_np.ndim != 3 or image_np.shape[-1] != 3:
+ raise ValueError(f"输入图像必须是 (h, w, 3) 的BGR数组,当前shape:{image_np.shape}")
+
+ if not isinstance(detection_results, list):
+ raise ValueError(f"detection_results必须是列表,当前类型:{type(detection_results)}")
+ for idx, result in enumerate(detection_results):
+ required_keys = {"class", "confidence", "bbox"}
+ if not isinstance(result, dict) or not required_keys.issubset(result.keys()):
+ raise ValueError(
+ f"detection_results第{idx}个元素格式错误,需包含键:{required_keys},"
+ f"当前元素:{result}"
+ )
+ bbox = result["bbox"]
+ if not (isinstance(bbox, (tuple, list)) and len(bbox) == 4 and all(isinstance(x, int) for x in bbox)):
+ raise ValueError(
+ f"detection_results第{idx}个元素的bbox格式错误,需为(x1,y1,x2,y2)整数元组,"
+ f"当前bbox:{bbox}"
+ )
+
+ #图像预处理(数据类型+通道)
+ draw_image = image_np.copy()
+ if draw_image.dtype != np.uint8:
+ draw_image = np.clip(draw_image * 255, 0, 255).astype(np.uint8)
+
+ #绘制边界框+标签
+ # 遍历所有检测结果,逐个绘制
+ for result in detection_results:
+ class_name = result["class"]
+ confidence = result["confidence"]
+ x1, y1, x2, y2 = result["bbox"]
+ cv2.rectangle(draw_image, (x1, y1), (x2, y2), color=(0, 255, 0), thickness=2)
+ label = f"{class_name}: {confidence:.2f}"
+ font = cv2.FONT_HERSHEY_SIMPLEX
+ font_scale = 0.5
+ font_thickness = 2
+ (label_width, label_height), baseline = cv2.getTextSize(
+ label, font, font_scale, font_thickness
+ )
+
+ bg_top_left = (x1, y1 - label_height - 10)
+ bg_bottom_right = (x1 + label_width, y1)
+ if bg_top_left[1] < 0:
+ bg_top_left = (x1, 0)
+ bg_bottom_right = (x1 + label_width, label_height + 10)
+ cv2.rectangle(draw_image, bg_top_left, bg_bottom_right, color=(0, 0, 0), thickness=-1)
+
+ text_origin = (x1, y1 - 5)
+ if bg_top_left[1] == 0:
+ text_origin = (x1, label_height + 5)
+ cv2.putText(
+ draw_image, label, text_origin,
+ font, font_scale, color=(255, 255, 255), thickness=font_thickness
+ )
+
+ #保存图片
+ try:
+ today = datetime.now()
+ year = today.strftime("%Y")
+ month = today.strftime("%m")
+ day = today.strftime("%d")
+ file_dir = os.path.join(
+ UPLOAD_ROOT, "detect", client_ip, file_type, year, month, day
+ )
+
+ #创建目录(若不存在则创建,支持多级目录)
+ os.makedirs(file_dir, exist_ok=True)
+
+ #生成唯一文件名
+ timestamp = today.strftime("%Y%m%d%H%M%S%f")
+ filename = f"{timestamp}.png"
+
+ # 4.4 构建完整保存路径和前端访问路径
+ full_path = os.path.join(file_dir, filename) # 本地完整路径
+ # 相对路径:移除UPLOAD_ROOT前缀,统一用"/"作为分隔符(兼容Windows/Linux)
+ relative_path = full_path.replace(UPLOAD_ROOT, "", 1).lstrip(os.sep)
+ download_path = PRE + relative_path.replace(os.sep, "/")
+
+ # 4.5 保存图片(CV2绘制的是BGR,需转RGB后用PIL保存,与原逻辑一致)
+ image_rgb = cv2.cvtColor(draw_image, cv2.COLOR_BGR2RGB)
+ img_pil = Image.fromarray(image_rgb)
+ img_pil.save(full_path, format="PNG", quality=95) # PNG格式无压缩,quality可忽略
+
+ print(f"YOLO检测图片保存成功 | 本地路径:{full_path} | 下载路径:{download_path}")
+ return download_path
+
+ except Exception as e:
+ raise Exception(f"YOLO检测图片保存失败:{str(e)}") from e
+
+
+def save_detect_face_file(
+ client_ip: str,
+ image_np: np.ndarray,
+ face_result: str,
+ file_type: str = "face",
+ matched_color: tuple = (0, 255, 0)
+) -> str:
+ """
+ 保存人脸识别结果图片(仅为「匹配成功」的人脸画框,标签不包含“匹配”二字)
+ """
+ #输入参数验证
+ if not isinstance(image_np, np.ndarray) or image_np.ndim != 3 or image_np.shape[-1] != 3:
+ raise ValueError(f"输入图像需为 (h, w, 3) 的BGR数组,当前shape:{image_np.shape}")
+ if not isinstance(face_result, str) or face_result.strip() == "":
+ raise ValueError("face_result必须是非空字符串")
+
+ # 解析face_result提取人脸信息
+ face_info_list = []
+ if face_result.strip() != "未检测到人脸":
+ face_pattern = re.compile(
+ r"(匹配|未匹配):\s*([^\s(]+)\s*\(相似度:\s*(\d+\.\d+),\s*边界框:\s*\[(\d+,\s*\d+,\s*\d+,\s*\d+)\]\)"
+ )
+ for part in [p.strip() for p in face_result.split(";") if p.strip()]:
+ match = face_pattern.match(part)
+ if match:
+ status, name, similarity, bbox_str = match.groups()
+ bbox = list(map(int, bbox_str.replace(" ", "").split(",")))
+ if len(bbox) == 4:
+ face_info_list.append({
+ "status": status,
+ "name": name,
+ "similarity": float(similarity),
+ "bbox": bbox
+ })
+
+ # 图像格式转换(OpenCV→PIL)
+ image_rgb = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
+ pil_img = Image.fromarray(image_rgb)
+ draw = ImageDraw.Draw(pil_img)
+
+ # 绘制边界框和标签
+ font_size = 12
+ try:
+ font = ImageFont.truetype("simhei", font_size)
+ except:
+ try:
+ font = ImageFont.truetype("simsun", font_size)
+ except:
+ font = ImageFont.load_default()
+ print("警告:未找到指定中文字体,使用PIL默认字体(可能影响中文显示)")
+
+ for face_info in face_info_list:
+ status = face_info["status"]
+ if status != "匹配":
+ print(f"跳过未匹配人脸:{face_info['name']}(相似度:{face_info['similarity']:.2f})")
+ continue
+
+ name = face_info["name"]
+ similarity = face_info["similarity"]
+ x1, y1, x2, y2 = face_info["bbox"]
+
+ # 4.1 绘制边界框(绿色)
+ img_cv = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
+ cv2.rectangle(img_cv, (x1, y1), (x2, y2), color=matched_color, thickness=2)
+ pil_img = Image.fromarray(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB))
+ draw = ImageDraw.Draw(pil_img)
+
+ label = f"{name} (相似度: {similarity:.2f})"
+
+ # 4.3 计算标签尺寸(文本变短后会自动适配,无需额外调整)
+ label_bbox = draw.textbbox((0, 0), label, font=font)
+ label_width = label_bbox[2] - label_bbox[0]
+ label_height = label_bbox[3] - label_bbox[1]
+
+ # 4.4 计算标签背景位置(避免超出图像)
+ bg_x1, bg_y1 = x1, y1 - label_height - 10
+ bg_x2, bg_y2 = x1 + label_width, y1
+ if bg_y1 < 0:
+ bg_y1, bg_y2 = y2 + 5, y2 + label_height + 15
+
+ # 4.5 绘制标签背景(黑色)和文本(白色)
+ draw.rectangle([(bg_x1, bg_y1), (bg_x2, bg_y2)], fill=(0, 0, 0))
+ text_x = bg_x1
+ text_y = bg_y1 if bg_y1 >= 0 else bg_y1 + label_height
+ draw.text((text_x, text_y), label, font=font, fill=(255, 255, 255))
+
+ #保存图片
+ try:
+ today = datetime.now()
+ file_dir = os.path.join(
+ UPLOAD_ROOT, "detect", client_ip, file_type,
+ today.strftime("%Y"), today.strftime("%m"), today.strftime("%d")
+ )
+ os.makedirs(file_dir, exist_ok=True)
+
+ timestamp = today.strftime("%Y%m%d%H%M%S%f")
+ filename = f"{timestamp}.png"
+ full_path = os.path.join(file_dir, filename)
+
+ pil_img.save(full_path, format="PNG", quality=100)
+
+ relative_path = full_path.replace(UPLOAD_ROOT, "", 1).lstrip(os.sep)
+ download_path = PRE + relative_path.replace(os.sep, "/")
+
+ matched_count = sum(1 for info in face_info_list if info["status"] == "匹配")
+ print(f"人脸检测图片保存成功 | 客户端IP:{client_ip} | 匹配人脸数:{matched_count} | 保存路径:{download_path}")
+ return download_path
+
+ except Exception as e:
+ raise Exception(f"人脸检测图片保存失败(客户端IP:{client_ip}):{str(e)}") from e
+
+def save_source_file(upload_file: UploadFile, file_type: str) -> str:
+ """保存上传的文件到source目录,返回下载路径"""
+ today = datetime.now()
+ year = today.strftime("%Y")
+ month = today.strftime("%m")
+ day = today.strftime("%d")
+
+ # 生成精确到微秒的时间戳,确保文件名唯一
+ timestamp = today.strftime("%Y%m%d%H%M%S%f")
+ # 构建新文件名:时间戳_原文件名
+ unique_filename = f"{timestamp}_{upload_file.filename}"
+
+ # 构建目录路径: upload/source/type/年/月/日(包含UPLOAD_ROOT)
+ file_dir = os.path.join(
+ UPLOAD_ROOT,
+ "source",
+ file_type,
+ year,
+ month,
+ day
+ )
+
+ # 创建目录(确保目录存在)
+ os.makedirs(file_dir, exist_ok=True)
+
+ # 1. 完整路径:用于实际保存文件(使用带时间戳的唯一文件名)
+ full_path = os.path.join(file_dir, unique_filename)
+ # 2. 相对路径:用于返回给前端
+ relative_path = full_path.replace(UPLOAD_ROOT, "", 1).lstrip(os.sep)
+
+ # 保存文件(使用完整路径)
+ try:
+ with open(full_path, "wb") as buffer:
+ shutil.copyfileobj(upload_file.file, buffer)
+ finally:
+ upload_file.file.close()
+
+ # 统一路径分隔符为/
+ return PRE + relative_path.replace(os.sep, "/")
+
+
+def get_absolute_path(relative_path: str) -> str:
+ """
+ 根据相对路径获取服务器上的绝对路径
+ """
+ path_without_pre = relative_path.replace(PRE, "", 1)
+
+ # 将相对路径转换为系统兼容的格式
+ normalized_path = os.path.normpath(path_without_pre)
+
+ # 拼接基础路径和相对路径,得到绝对路径
+ absolute_path = os.path.abspath(os.path.join(UPLOAD_ROOT, normalized_path))
+
+ # 安全检查:确保生成的路径在UPLOAD_ROOT目录下,防止路径遍历
+ if not absolute_path.startswith(os.path.abspath(UPLOAD_ROOT)):
+ raise ValueError("无效的相对路径,可能试图访问上传目录之外的内容")
+
+ return absolute_path
\ No newline at end of file
diff --git a/service/model_service.py b/service/model_service.py
new file mode 100644
index 0000000..33a3a06
--- /dev/null
+++ b/service/model_service.py
@@ -0,0 +1,131 @@
+from http.client import HTTPException
+
+import numpy as np
+import torch
+from MySQLdb import MySQLError
+from ultralytics import YOLO
+import os
+
+from ds.db import db
+from service.file_service import get_absolute_path
+
+# 全局变量
+current_yolo_model = None
+current_model_absolute_path = None # 存储模型绝对路径,不依赖model实例
+
+ALLOWED_MODEL_EXT = {"pt"}
+MAX_MODEL_SIZE = 100 * 1024 * 1024 # 100MB
+
+
+def load_yolo_model():
+ """加载模型并存储绝对路径"""
+ global current_yolo_model, current_model_absolute_path
+ model_rel_path = get_enabled_model_rel_path()
+ print(f"[模型初始化] 加载模型:{model_rel_path}")
+
+ # 计算并存储绝对路径
+ current_model_absolute_path = get_absolute_path(model_rel_path)
+ print(f"[模型初始化] 绝对路径:{current_model_absolute_path}")
+
+ # 检查模型文件
+ if not os.path.exists(current_model_absolute_path):
+ raise FileNotFoundError(f"模型文件不存在: {current_model_absolute_path}")
+
+ try:
+ new_model = YOLO(current_model_absolute_path)
+ if torch.cuda.is_available():
+ new_model.to('cuda')
+ print("模型已移动到GPU")
+ else:
+ print("使用CPU进行推理")
+ current_yolo_model = new_model
+ print(f"成功加载模型: {current_model_absolute_path}")
+ return current_yolo_model
+ except Exception as e:
+ print(f"模型加载失败:{str(e)}")
+ raise
+
+
+def get_current_model():
+ """获取当前模型实例"""
+ if current_yolo_model is None:
+ raise ValueError("尚未加载任何YOLO模型,请先调用load_yolo_model加载模型")
+ return current_yolo_model
+
+
+def detect(image_np, conf_threshold=0.8):
+ # 1. 输入格式验证
+ if not isinstance(image_np, np.ndarray):
+ raise ValueError("输入必须是numpy数组(BGR图像)")
+ if image_np.ndim != 3 or image_np.shape[-1] != 3:
+ raise ValueError(f"输入图像格式错误,需为 (h, w, 3) 的BGR数组,当前shape: {image_np.shape}")
+ detection_results = []
+ try:
+ model = get_current_model()
+ if not current_model_absolute_path:
+ raise RuntimeError("模型未初始化!请先调用 load_yolo_model 加载模型")
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ print(f"检测设备:{device} | 置信度阈值:{conf_threshold}")
+
+ # 图像尺寸信息
+ img_height, img_width = image_np.shape[:2]
+ print(f"输入图像尺寸:{img_width}x{img_height}")
+
+ # YOLO检测
+ print("执行YOLO检测")
+ results = model.predict(
+ image_np,
+ conf=conf_threshold,
+ device=device,
+ show=False,
+ )
+
+ # 4. 整理检测结果(仅保留Chest类别,ID=2)
+ for box in results[0].boxes:
+ class_id = int(box.cls[0]) # 类别ID
+ class_name = model.names[class_id]
+ confidence = float(box.conf[0])
+ bbox = tuple(map(int, box.xyxy[0]))
+
+ # 过滤条件:置信度达标 + 类别为Chest(class_id=2)
+ # and class_id == 2
+ if confidence >= conf_threshold:
+ detection_results.append({
+ "class": class_name,
+ "confidence": confidence,
+ "bbox": bbox
+ })
+
+ # 判断是否有目标
+ has_content = len(detection_results) > 0
+ return has_content, detection_results
+
+ except Exception as e:
+ error_msg = f"检测过程出错:{str(e)}"
+ print(error_msg)
+ return False, None
+
+
+def get_enabled_model_rel_path():
+ """获取数据库中启用的模型相对路径"""
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+ query = "SELECT path FROM model WHERE is_default = 1 LIMIT 1"
+ cursor.execute(query)
+ result = cursor.fetchone()
+
+ if not result or not result.get('path'):
+ raise HTTPException(status_code=404, detail="未找到启用的默认模型")
+
+ return result['path']
+ except MySQLError as e:
+ raise HTTPException(status_code=500, detail=f"查询默认模型时发生数据库错误:{str(e)}") from e
+ except Exception as e:
+ if isinstance(e, HTTPException):
+ raise e
+ raise HTTPException(status_code=500, detail=f"获取默认模型路径失败:{str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
\ No newline at end of file
diff --git a/service/ocr_service.py b/service/ocr_service.py
new file mode 100644
index 0000000..fa9cfcd
--- /dev/null
+++ b/service/ocr_service.py
@@ -0,0 +1,131 @@
+# 首先添加NumPy兼容处理
+import numpy as np
+
+# 修复np.int已弃用的问题
+if not hasattr(np, 'int'):
+ np.int = int
+
+from paddleocr import PaddleOCR
+from service.sensitive_service import get_all_sensitive_words
+
+_ocr_engine = None
+_forbidden_words = set()
+_conf_threshold = 0.5
+
+def set_forbidden_words(new_words):
+ global _forbidden_words
+ if not isinstance(new_words, (set, list, tuple)):
+ raise TypeError("新违禁词必须是集合、列表或元组类型")
+ _forbidden_words = set(new_words) # 确保是集合类型
+ print(f"已通过函数更新违禁词,当前数量: {len(_forbidden_words)}")
+
+def load_forbidden_words():
+ global _forbidden_words
+ try:
+ _forbidden_words = get_all_sensitive_words()
+ print(f"加载的违禁词数量: {len(_forbidden_words)}")
+ except Exception as e:
+ print(f"Forbidden words load error: {e}")
+ return False
+ return True
+
+
+def init_ocr_engine():
+ global _ocr_engine
+ try:
+ _ocr_engine = PaddleOCR(
+ use_angle_cls=True,
+ lang="ch",
+ show_log=False,
+ use_gpu=True,
+ max_text_length=1024
+ )
+ load_result = load_forbidden_words()
+ if not load_result:
+ print("警告:违禁词加载失败,可能影响检测功能")
+ print("OCR引擎初始化完成")
+ return True
+ except Exception as e:
+ print(f"OCR引擎初始化错误: {e}")
+ _ocr_engine = None
+ return False
+
+
+def detect(frame, conf_threshold=0.8):
+ print("开始进行OCR检测...")
+ try:
+ ocr_res = _ocr_engine.ocr(frame, cls=True)
+ if not ocr_res or not isinstance(ocr_res, list):
+ return (False, "无OCR结果")
+
+ texts = []
+ confs = []
+ for line in ocr_res:
+ if line is None:
+ continue
+ if isinstance(line, list):
+ items_to_process = line
+ else:
+ items_to_process = [line]
+
+ for item in items_to_process:
+ if isinstance(item, list) and len(item) == 4:
+ is_coordinate = True
+ for point in item:
+ if not (isinstance(point, list) and len(point) == 2 and
+ all(isinstance(coord, (int, float)) for coord in point)):
+ is_coordinate = False
+ break
+ if is_coordinate:
+ continue
+ if isinstance(item, list) and all(isinstance(x, (int, float)) for x in item):
+ continue
+ if isinstance(item, tuple) and len(item) == 2:
+ text, conf = item
+ if isinstance(text, str) and isinstance(conf, (int, float)):
+ texts.append(text.strip())
+ confs.append(float(conf))
+ continue
+ if isinstance(item, list) and len(item) >= 2:
+ text_data = item[1]
+ if isinstance(text_data, tuple) and len(text_data) == 2:
+ text, conf = text_data
+ if isinstance(text, str) and isinstance(conf, (int, float)):
+ texts.append(text.strip())
+ confs.append(float(conf))
+ continue
+ elif isinstance(text_data, str):
+ texts.append(text_data.strip())
+ confs.append(1.0)
+ continue
+ print(f"无法解析的OCR结果格式: {item}")
+
+ if len(texts) != len(confs):
+ return (False, "OCR结果格式异常")
+
+ # 收集所有识别到的违禁词(去重且保持出现顺序)
+ vio_words = []
+ for txt, conf in zip(texts, confs):
+ if conf < _conf_threshold: # 过滤低置信度结果
+ continue
+ # 提取当前文本中包含的违禁词
+ matched = [w for w in _forbidden_words if w in txt]
+ # 仅添加未记录过的违禁词(去重)
+ for word in matched:
+ if word not in vio_words:
+ vio_words.append(word)
+
+ has_text = len(texts) > 0
+ has_violation = len(vio_words) > 0
+
+ if not has_text:
+ return (False, "未识别到文本")
+ elif has_violation:
+ # 多个违禁词用逗号拼接
+ return (True, ", ".join(vio_words))
+ else:
+ return (False, "未检测到违禁词")
+
+ except Exception as e:
+ print(f"OCR detect error: {e}")
+ return (False, f"检测错误: {str(e)}")
\ No newline at end of file
diff --git a/service/sensitive_service.py b/service/sensitive_service.py
new file mode 100644
index 0000000..6f7d5db
--- /dev/null
+++ b/service/sensitive_service.py
@@ -0,0 +1,36 @@
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+
+
+def get_all_sensitive_words() -> list[str]:
+ """
+ 获取所有敏感词(返回纯字符串列表、用于过滤业务)
+
+ 返回:
+ list[str]: 包含所有敏感词的数组
+
+ 异常:
+ MySQLError: 数据库操作相关错误
+ """
+ conn = None
+ cursor = None
+ try:
+ # 获取数据库连接
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ # 执行查询(只获取敏感词字段、按ID排序)
+ query = "SELECT name FROM sensitives ORDER BY id"
+ cursor.execute(query)
+ sensitive_records = cursor.fetchall()
+
+ # 提取敏感词到纯字符串数组
+ return [record['name'] for record in sensitive_records]
+
+ except MySQLError as e:
+ # 数据库错误向上抛出、由调用方处理
+ raise MySQLError(f"查询敏感词列表失败: {str(e)}") from e
+ finally:
+ # 确保数据库连接正确释放
+ db.close_connection(conn, cursor)
\ No newline at end of file
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173159642732.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173159642732.png
new file mode 100644
index 0000000..1f509da
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173159642732.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173208145173.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173208145173.png
new file mode 100644
index 0000000..dcac18c
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173208145173.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173217730786.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173217730786.png
new file mode 100644
index 0000000..02edbf0
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173217730786.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173238371986.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173238371986.png
new file mode 100644
index 0000000..0329daf
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173238371986.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173246422824.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173246422824.png
new file mode 100644
index 0000000..ba4724e
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173246422824.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173258341995.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173258341995.png
new file mode 100644
index 0000000..212bd73
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173258341995.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173259916277.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173259916277.png
new file mode 100644
index 0000000..ece2a94
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173259916277.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173304495074.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173304495074.png
new file mode 100644
index 0000000..8c2d8cc
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173304495074.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173305693185.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173305693185.png
new file mode 100644
index 0000000..41ec858
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173305693185.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173308932856.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173308932856.png
new file mode 100644
index 0000000..830dcaf
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173308932856.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173314842313.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173314842313.png
new file mode 100644
index 0000000..9db7b11
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173314842313.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173530981192.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173530981192.png
new file mode 100644
index 0000000..e63bdd4
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173530981192.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173535212652.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173535212652.png
new file mode 100644
index 0000000..c3d3e6f
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173535212652.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173541998006.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173541998006.png
new file mode 100644
index 0000000..e2d6362
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173541998006.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926173612038840.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926173612038840.png
new file mode 100644
index 0000000..f40a3f2
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926173612038840.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926174548202204.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926174548202204.png
new file mode 100644
index 0000000..f3d3478
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926174548202204.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926174553788454.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926174553788454.png
new file mode 100644
index 0000000..ce46849
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926174553788454.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926180607332421.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926180607332421.png
new file mode 100644
index 0000000..ce941dd
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926180607332421.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926180632968203.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926180632968203.png
new file mode 100644
index 0000000..c6e596c
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926180632968203.png differ
diff --git a/upload/detect/192.168.110.113/face/2025/09/26/20250926180640964513.png b/upload/detect/192.168.110.113/face/2025/09/26/20250926180640964513.png
new file mode 100644
index 0000000..042a9ff
Binary files /dev/null and b/upload/detect/192.168.110.113/face/2025/09/26/20250926180640964513.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172120357791.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172120357791.png
new file mode 100644
index 0000000..218b6bd
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172120357791.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172149576720.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172149576720.png
new file mode 100644
index 0000000..f6b9240
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172149576720.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172158859757.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172158859757.png
new file mode 100644
index 0000000..6af3cdc
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172158859757.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172215286659.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172215286659.png
new file mode 100644
index 0000000..1ebd57a
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926172215286659.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180229562532.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180229562532.png
new file mode 100644
index 0000000..ebf2e41
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180229562532.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180248914056.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180248914056.png
new file mode 100644
index 0000000..f4677a1
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180248914056.png differ
diff --git a/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180255323760.png b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180255323760.png
new file mode 100644
index 0000000..af9409f
Binary files /dev/null and b/upload/detect/192.168.110.113/ocr/2025/09/26/20250926180255323760.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172341628017.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172341628017.png
new file mode 100644
index 0000000..e2d8697
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172341628017.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172810115920.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172810115920.png
new file mode 100644
index 0000000..6299953
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172810115920.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172833394701.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172833394701.png
new file mode 100644
index 0000000..8cc4c90
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172833394701.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172849935998.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172849935998.png
new file mode 100644
index 0000000..166c0ea
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172849935998.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172917752666.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172917752666.png
new file mode 100644
index 0000000..61a5dc7
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172917752666.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172934770489.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172934770489.png
new file mode 100644
index 0000000..1b7c995
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926172934770489.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173021396099.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173021396099.png
new file mode 100644
index 0000000..746cc99
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173021396099.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173037870795.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173037870795.png
new file mode 100644
index 0000000..746cc99
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173037870795.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173042951909.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173042951909.png
new file mode 100644
index 0000000..77d97fd
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173042951909.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173122436584.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173122436584.png
new file mode 100644
index 0000000..665cd84
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173122436584.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173705936011.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173705936011.png
new file mode 100644
index 0000000..2fc4831
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173705936011.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173713283955.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173713283955.png
new file mode 100644
index 0000000..5d5e2a2
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926173713283955.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174438405350.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174438405350.png
new file mode 100644
index 0000000..4fdd153
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174438405350.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174447804246.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174447804246.png
new file mode 100644
index 0000000..33c5153
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174447804246.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174502496784.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174502496784.png
new file mode 100644
index 0000000..ce5087e
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174502496784.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174508244002.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174508244002.png
new file mode 100644
index 0000000..dbeae80
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174508244002.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174528939720.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174528939720.png
new file mode 100644
index 0000000..91b9d1d
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174528939720.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174923147095.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174923147095.png
new file mode 100644
index 0000000..742f1c4
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174923147095.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174930964081.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174930964081.png
new file mode 100644
index 0000000..579da50
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174930964081.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174944276063.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174944276063.png
new file mode 100644
index 0000000..4de8f49
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926174944276063.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175004836029.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175004836029.png
new file mode 100644
index 0000000..68f835f
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175004836029.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175030000795.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175030000795.png
new file mode 100644
index 0000000..1bdd56b
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175030000795.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175036819109.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175036819109.png
new file mode 100644
index 0000000..7395379
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175036819109.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175218999791.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175218999791.png
new file mode 100644
index 0000000..bfb80f5
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175218999791.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175225788930.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175225788930.png
new file mode 100644
index 0000000..cbcc1f0
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175225788930.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175331289844.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175331289844.png
new file mode 100644
index 0000000..3c404bd
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175331289844.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175453449650.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175453449650.png
new file mode 100644
index 0000000..9762a71
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175453449650.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175655715865.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175655715865.png
new file mode 100644
index 0000000..53dc795
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175655715865.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175731083077.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175731083077.png
new file mode 100644
index 0000000..978acbc
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175731083077.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175747051954.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175747051954.png
new file mode 100644
index 0000000..357a20e
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175747051954.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175931979324.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175931979324.png
new file mode 100644
index 0000000..da4bb3a
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926175931979324.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180121414651.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180121414651.png
new file mode 100644
index 0000000..8fb6093
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180121414651.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180145892614.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180145892614.png
new file mode 100644
index 0000000..b9da28f
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180145892614.png differ
diff --git a/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180218138100.png b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180218138100.png
new file mode 100644
index 0000000..2c11118
Binary files /dev/null and b/upload/detect/192.168.110.113/yolo/2025/09/26/20250926180218138100.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928114858711518.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928114858711518.png
new file mode 100644
index 0000000..33c990a
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928114858711518.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928120637209246.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928120637209246.png
new file mode 100644
index 0000000..e81c343
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928120637209246.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928125651342793.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928125651342793.png
new file mode 100644
index 0000000..931512f
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928125651342793.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928130653196594.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928130653196594.png
new file mode 100644
index 0000000..631aff4
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928130653196594.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928130658889518.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928130658889518.png
new file mode 100644
index 0000000..6531e40
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928130658889518.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928130704875870.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928130704875870.png
new file mode 100644
index 0000000..3931d10
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928130704875870.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928134021558833.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928134021558833.png
new file mode 100644
index 0000000..6da05bd
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928134021558833.png differ
diff --git a/upload/detect/192.168.110.25/face/2025/09/28/20250928134026678865.png b/upload/detect/192.168.110.25/face/2025/09/28/20250928134026678865.png
new file mode 100644
index 0000000..9808953
Binary files /dev/null and b/upload/detect/192.168.110.25/face/2025/09/28/20250928134026678865.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928103220048163.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928103220048163.png
new file mode 100644
index 0000000..6119fef
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928103220048163.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928104412287251.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928104412287251.png
new file mode 100644
index 0000000..85c340d
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928104412287251.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110444975047.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110444975047.png
new file mode 100644
index 0000000..5c4a327
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110444975047.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110551625267.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110551625267.png
new file mode 100644
index 0000000..50e7960
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110551625267.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110615035349.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110615035349.png
new file mode 100644
index 0000000..7fe3f7a
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110615035349.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110929375051.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110929375051.png
new file mode 100644
index 0000000..a393487
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110929375051.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110946604166.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110946604166.png
new file mode 100644
index 0000000..b1bda2f
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928110946604166.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928111125801035.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928111125801035.png
new file mode 100644
index 0000000..350d0b0
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928111125801035.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113024240830.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113024240830.png
new file mode 100644
index 0000000..df492e5
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113024240830.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113510404606.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113510404606.png
new file mode 100644
index 0000000..fe30dc4
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113510404606.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113916790569.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113916790569.png
new file mode 100644
index 0000000..ec03a33
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928113916790569.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928114817588441.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928114817588441.png
new file mode 100644
index 0000000..fc8717b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928114817588441.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120012180356.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120012180356.png
new file mode 100644
index 0000000..cd017e6
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120012180356.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120107917628.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120107917628.png
new file mode 100644
index 0000000..9c2be4c
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120107917628.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120419126619.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120419126619.png
new file mode 100644
index 0000000..02ed5b0
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120419126619.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120438747199.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120438747199.png
new file mode 100644
index 0000000..c304c8f
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120438747199.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120453964738.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120453964738.png
new file mode 100644
index 0000000..1a3f94f
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120453964738.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120506330418.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120506330418.png
new file mode 100644
index 0000000..ebdd98b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120506330418.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120620250653.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120620250653.png
new file mode 100644
index 0000000..705d612
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120620250653.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120654334183.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120654334183.png
new file mode 100644
index 0000000..eea95a1
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120654334183.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120710321793.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120710321793.png
new file mode 100644
index 0000000..f3893e6
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120710321793.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120753691902.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120753691902.png
new file mode 100644
index 0000000..d1ae747
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928120753691902.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121704698941.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121704698941.png
new file mode 100644
index 0000000..bfa9b37
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121704698941.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121720115790.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121720115790.png
new file mode 100644
index 0000000..9b1110d
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121720115790.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121751767970.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121751767970.png
new file mode 100644
index 0000000..7e2f0a1
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121751767970.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121801129097.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121801129097.png
new file mode 100644
index 0000000..db4fae5
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121801129097.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121808821992.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121808821992.png
new file mode 100644
index 0000000..3764747
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121808821992.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121825427915.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121825427915.png
new file mode 100644
index 0000000..78a2c1a
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121825427915.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121832675224.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121832675224.png
new file mode 100644
index 0000000..9fbc9ac
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121832675224.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121833085700.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121833085700.png
new file mode 100644
index 0000000..9fbc9ac
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928121833085700.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928122742303000.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928122742303000.png
new file mode 100644
index 0000000..c4923d8
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928122742303000.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928123030770910.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928123030770910.png
new file mode 100644
index 0000000..8512c71
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928123030770910.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125532612689.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125532612689.png
new file mode 100644
index 0000000..14c2949
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125532612689.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125556594713.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125556594713.png
new file mode 100644
index 0000000..42af59b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125556594713.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125602732105.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125602732105.png
new file mode 100644
index 0000000..ae8e701
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125602732105.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125604149563.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125604149563.png
new file mode 100644
index 0000000..846deaa
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125604149563.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125608883741.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125608883741.png
new file mode 100644
index 0000000..e899498
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125608883741.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125612979068.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125612979068.png
new file mode 100644
index 0000000..ef4e65b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125612979068.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125635958778.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125635958778.png
new file mode 100644
index 0000000..adfa12f
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125635958778.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125738322301.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125738322301.png
new file mode 100644
index 0000000..297c363
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928125738322301.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130103995726.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130103995726.png
new file mode 100644
index 0000000..1cc286b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130103995726.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130111408609.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130111408609.png
new file mode 100644
index 0000000..de250bd
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130111408609.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130132952112.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130132952112.png
new file mode 100644
index 0000000..fa370a0
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130132952112.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130140324017.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130140324017.png
new file mode 100644
index 0000000..96e8f29
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130140324017.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130154964660.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130154964660.png
new file mode 100644
index 0000000..98ace3b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130154964660.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130200418981.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130200418981.png
new file mode 100644
index 0000000..214b47f
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130200418981.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130355229989.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130355229989.png
new file mode 100644
index 0000000..4c0d54b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130355229989.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130417242969.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130417242969.png
new file mode 100644
index 0000000..4b05907
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130417242969.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130425156315.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130425156315.png
new file mode 100644
index 0000000..74fff50
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130425156315.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130445779845.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130445779845.png
new file mode 100644
index 0000000..7759248
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130445779845.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130459963753.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130459963753.png
new file mode 100644
index 0000000..d89b663
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130459963753.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130505382367.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130505382367.png
new file mode 100644
index 0000000..53901d8
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130505382367.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130535717782.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130535717782.png
new file mode 100644
index 0000000..2e66678
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130535717782.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130551778345.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130551778345.png
new file mode 100644
index 0000000..81b3e00
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130551778345.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130558531877.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130558531877.png
new file mode 100644
index 0000000..8e70109
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130558531877.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130601159046.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130601159046.png
new file mode 100644
index 0000000..e163de2
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130601159046.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130603002945.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130603002945.png
new file mode 100644
index 0000000..42b33c8
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130603002945.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130609108553.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130609108553.png
new file mode 100644
index 0000000..5beb9d0
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130609108553.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130613667883.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130613667883.png
new file mode 100644
index 0000000..cf899c4
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130613667883.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130617463319.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130617463319.png
new file mode 100644
index 0000000..f740488
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130617463319.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130627060876.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130627060876.png
new file mode 100644
index 0000000..f2b6920
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130627060876.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130632966114.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130632966114.png
new file mode 100644
index 0000000..dc34fb1
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928130632966114.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133519011288.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133519011288.png
new file mode 100644
index 0000000..90e71b0
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133519011288.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133705511037.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133705511037.png
new file mode 100644
index 0000000..9e8b270
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133705511037.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133710006029.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133710006029.png
new file mode 100644
index 0000000..741e2ae
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133710006029.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133724310806.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133724310806.png
new file mode 100644
index 0000000..36573fd
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133724310806.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133731457808.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133731457808.png
new file mode 100644
index 0000000..d5a46c8
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133731457808.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133743588993.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133743588993.png
new file mode 100644
index 0000000..5481035
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133743588993.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133748399542.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133748399542.png
new file mode 100644
index 0000000..1b22235
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133748399542.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133753629564.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133753629564.png
new file mode 100644
index 0000000..c772013
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133753629564.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133757848120.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133757848120.png
new file mode 100644
index 0000000..8450bcd
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133757848120.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133844776041.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133844776041.png
new file mode 100644
index 0000000..ebdf102
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928133844776041.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134005933767.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134005933767.png
new file mode 100644
index 0000000..744531b
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134005933767.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134104349797.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134104349797.png
new file mode 100644
index 0000000..62bf1bb
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134104349797.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134216421594.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134216421594.png
new file mode 100644
index 0000000..a0ad0e0
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134216421594.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134440696394.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134440696394.png
new file mode 100644
index 0000000..3f33661
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134440696394.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134533685876.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134533685876.png
new file mode 100644
index 0000000..bf07d47
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134533685876.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134549578744.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134549578744.png
new file mode 100644
index 0000000..ef6270a
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134549578744.png differ
diff --git a/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134554228918.png b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134554228918.png
new file mode 100644
index 0000000..a788b36
Binary files /dev/null and b/upload/detect/192.168.110.25/ocr/2025/09/28/20250928134554228918.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928125823305354.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928125823305354.png
new file mode 100644
index 0000000..5505f97
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928125823305354.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130003673377.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130003673377.png
new file mode 100644
index 0000000..d662b24
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130003673377.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130011379347.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130011379347.png
new file mode 100644
index 0000000..fe87808
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130011379347.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130018955974.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130018955974.png
new file mode 100644
index 0000000..5616a0a
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130018955974.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130023537349.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130023537349.png
new file mode 100644
index 0000000..a4f91ca
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130023537349.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130032913472.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130032913472.png
new file mode 100644
index 0000000..153522c
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928130032913472.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133441931429.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133441931429.png
new file mode 100644
index 0000000..d069f8d
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133441931429.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133456004300.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133456004300.png
new file mode 100644
index 0000000..cc39df7
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133456004300.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133502882031.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133502882031.png
new file mode 100644
index 0000000..7ca8bed
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133502882031.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133524440836.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133524440836.png
new file mode 100644
index 0000000..bd71f8e
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133524440836.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133529602202.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133529602202.png
new file mode 100644
index 0000000..91deb9f
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133529602202.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133535947870.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133535947870.png
new file mode 100644
index 0000000..540fd05
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133535947870.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133803288423.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133803288423.png
new file mode 100644
index 0000000..c90f4f2
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133803288423.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133806764504.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133806764504.png
new file mode 100644
index 0000000..23e20d2
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133806764504.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133810294869.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133810294869.png
new file mode 100644
index 0000000..0f34db9
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133810294869.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133815340333.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133815340333.png
new file mode 100644
index 0000000..fc8da7d
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133815340333.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133820223861.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133820223861.png
new file mode 100644
index 0000000..010e885
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928133820223861.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134317717680.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134317717680.png
new file mode 100644
index 0000000..75676fa
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134317717680.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134334158998.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134334158998.png
new file mode 100644
index 0000000..ced58b2
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134334158998.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134455318808.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134455318808.png
new file mode 100644
index 0000000..15b7d9e
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134455318808.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134524381847.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134524381847.png
new file mode 100644
index 0000000..3ea8c8e
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134524381847.png differ
diff --git a/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134540534007.png b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134540534007.png
new file mode 100644
index 0000000..466c6dc
Binary files /dev/null and b/upload/detect/192.168.110.25/yolo/2025/09/28/20250928134540534007.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/23/20250923173926189309.png b/upload/detect/192.168.110.31/face/2025/09/23/20250923173926189309.png
new file mode 100644
index 0000000..6a03180
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/23/20250923173926189309.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/23/20250923173928083638.png b/upload/detect/192.168.110.31/face/2025/09/23/20250923173928083638.png
new file mode 100644
index 0000000..dbf07b2
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/23/20250923173928083638.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/23/20250923174319986928.png b/upload/detect/192.168.110.31/face/2025/09/23/20250923174319986928.png
new file mode 100644
index 0000000..623e5b0
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/23/20250923174319986928.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/25/20250925161000554731.png b/upload/detect/192.168.110.31/face/2025/09/25/20250925161000554731.png
new file mode 100644
index 0000000..af49c35
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/25/20250925161000554731.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/25/20250925171526112234.png b/upload/detect/192.168.110.31/face/2025/09/25/20250925171526112234.png
new file mode 100644
index 0000000..7136d99
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/25/20250925171526112234.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/25/20250925171528364900.png b/upload/detect/192.168.110.31/face/2025/09/25/20250925171528364900.png
new file mode 100644
index 0000000..509bf4c
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/25/20250925171528364900.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/25/20250925171946908917.png b/upload/detect/192.168.110.31/face/2025/09/25/20250925171946908917.png
new file mode 100644
index 0000000..32ed05e
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/25/20250925171946908917.png differ
diff --git a/upload/detect/192.168.110.31/face/2025/09/28/20250928104330842539.png b/upload/detect/192.168.110.31/face/2025/09/28/20250928104330842539.png
new file mode 100644
index 0000000..ca7104e
Binary files /dev/null and b/upload/detect/192.168.110.31/face/2025/09/28/20250928104330842539.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/23/20250923171632386080.png b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923171632386080.png
new file mode 100644
index 0000000..7435413
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923171632386080.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172112934476.png b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172112934476.png
new file mode 100644
index 0000000..43feba1
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172112934476.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172502698948.png b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172502698948.png
new file mode 100644
index 0000000..300e885
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172502698948.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172717032136.png b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172717032136.png
new file mode 100644
index 0000000..01755f1
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/23/20250923172717032136.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/25/20250925095406932348.png b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925095406932348.png
new file mode 100644
index 0000000..fb8cbf7
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925095406932348.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/25/20250925165506917004.png b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925165506917004.png
new file mode 100644
index 0000000..d33d98b
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925165506917004.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/25/20250925170751652791.png b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925170751652791.png
new file mode 100644
index 0000000..b09bd9f
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925170751652791.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/25/20250925171205793467.png b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925171205793467.png
new file mode 100644
index 0000000..77f2277
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/25/20250925171205793467.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/26/20250926181246583587.png b/upload/detect/192.168.110.31/ocr/2025/09/26/20250926181246583587.png
new file mode 100644
index 0000000..f04be17
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/26/20250926181246583587.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/26/20250926181247478533.png b/upload/detect/192.168.110.31/ocr/2025/09/26/20250926181247478533.png
new file mode 100644
index 0000000..11999e9
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/26/20250926181247478533.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/26/20250926182108844852.png b/upload/detect/192.168.110.31/ocr/2025/09/26/20250926182108844852.png
new file mode 100644
index 0000000..b6980de
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/26/20250926182108844852.png differ
diff --git a/upload/detect/192.168.110.31/ocr/2025/09/28/20250928103824778425.png b/upload/detect/192.168.110.31/ocr/2025/09/28/20250928103824778425.png
new file mode 100644
index 0000000..1413d86
Binary files /dev/null and b/upload/detect/192.168.110.31/ocr/2025/09/28/20250928103824778425.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/23/20250923174509952032.png b/upload/detect/192.168.110.31/yolo/2025/09/23/20250923174509952032.png
new file mode 100644
index 0000000..f91201f
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/23/20250923174509952032.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/24/20250924103739025042.png b/upload/detect/192.168.110.31/yolo/2025/09/24/20250924103739025042.png
new file mode 100644
index 0000000..d80f364
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/24/20250924103739025042.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925094246962854.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925094246962854.png
new file mode 100644
index 0000000..af4ae1d
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925094246962854.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925094749265809.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925094749265809.png
new file mode 100644
index 0000000..4c93719
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925094749265809.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095014413688.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095014413688.png
new file mode 100644
index 0000000..a200156
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095014413688.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095314853971.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095314853971.png
new file mode 100644
index 0000000..45ca454
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095314853971.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095709567923.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095709567923.png
new file mode 100644
index 0000000..96b029d
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925095709567923.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100454465724.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100454465724.png
new file mode 100644
index 0000000..c51c043
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100454465724.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100623982133.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100623982133.png
new file mode 100644
index 0000000..422f1c2
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100623982133.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100848748381.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100848748381.png
new file mode 100644
index 0000000..0ce303e
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925100848748381.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101022341516.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101022341516.png
new file mode 100644
index 0000000..8508656
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101022341516.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101211780974.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101211780974.png
new file mode 100644
index 0000000..3e7d7d1
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101211780974.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101349140135.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101349140135.png
new file mode 100644
index 0000000..f276359
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101349140135.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101506197544.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101506197544.png
new file mode 100644
index 0000000..9843bdd
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925101506197544.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925171415050092.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925171415050092.png
new file mode 100644
index 0000000..33b9d29
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925171415050092.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925172854326109.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925172854326109.png
new file mode 100644
index 0000000..d67358b
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925172854326109.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925173522140164.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925173522140164.png
new file mode 100644
index 0000000..03d2001
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925173522140164.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925173748778781.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925173748778781.png
new file mode 100644
index 0000000..c2e6d33
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925173748778781.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/25/20250925174129013869.png b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925174129013869.png
new file mode 100644
index 0000000..9a9d9c9
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/25/20250925174129013869.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926092459085127.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926092459085127.png
new file mode 100644
index 0000000..483f7c1
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926092459085127.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115622058696.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115622058696.png
new file mode 100644
index 0000000..48a760a
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115622058696.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115754189181.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115754189181.png
new file mode 100644
index 0000000..624a10d
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115754189181.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115912577718.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115912577718.png
new file mode 100644
index 0000000..6cc170f
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926115912577718.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926120055763099.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926120055763099.png
new file mode 100644
index 0000000..a3ad335
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926120055763099.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926120205575936.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926120205575936.png
new file mode 100644
index 0000000..8246ab5
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926120205575936.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926141046437261.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926141046437261.png
new file mode 100644
index 0000000..5638385
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926141046437261.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926143523578870.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926143523578870.png
new file mode 100644
index 0000000..2e36581
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926143523578870.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926144216195011.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926144216195011.png
new file mode 100644
index 0000000..7d98f33
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926144216195011.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145116294262.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145116294262.png
new file mode 100644
index 0000000..1548694
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145116294262.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145221360917.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145221360917.png
new file mode 100644
index 0000000..4922480
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145221360917.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145354457952.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145354457952.png
new file mode 100644
index 0000000..de9eee3
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926145354457952.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151100965912.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151100965912.png
new file mode 100644
index 0000000..21e2bf8
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151100965912.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151228538925.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151228538925.png
new file mode 100644
index 0000000..b82ffef
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151228538925.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151407701454.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151407701454.png
new file mode 100644
index 0000000..f21ec4b
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151407701454.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151914513750.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151914513750.png
new file mode 100644
index 0000000..6098be4
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926151914513750.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152114459155.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152114459155.png
new file mode 100644
index 0000000..3f721f1
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152114459155.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152154120061.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152154120061.png
new file mode 100644
index 0000000..d180c82
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152154120061.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152314019913.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152314019913.png
new file mode 100644
index 0000000..947f0ae
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152314019913.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152548498603.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152548498603.png
new file mode 100644
index 0000000..9fcc99e
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152548498603.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152910609792.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152910609792.png
new file mode 100644
index 0000000..0b9ddd0
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926152910609792.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926153101377318.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926153101377318.png
new file mode 100644
index 0000000..2719906
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926153101377318.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162427890192.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162427890192.png
new file mode 100644
index 0000000..a8a6582
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162427890192.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162640521215.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162640521215.png
new file mode 100644
index 0000000..dc4f314
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162640521215.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162834800695.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162834800695.png
new file mode 100644
index 0000000..13e8eb5
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926162834800695.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163158804190.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163158804190.png
new file mode 100644
index 0000000..a7c5630
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163158804190.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163326359843.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163326359843.png
new file mode 100644
index 0000000..98cfc29
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163326359843.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163444892375.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163444892375.png
new file mode 100644
index 0000000..189cc34
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926163444892375.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/26/20250926181403456896.png b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926181403456896.png
new file mode 100644
index 0000000..194c44f
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/26/20250926181403456896.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928124425681381.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928124425681381.png
new file mode 100644
index 0000000..5c45677
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928124425681381.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928124933810457.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928124933810457.png
new file mode 100644
index 0000000..d53d943
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928124933810457.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928125059846489.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928125059846489.png
new file mode 100644
index 0000000..654e61d
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928125059846489.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928131208005723.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928131208005723.png
new file mode 100644
index 0000000..a31833c
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928131208005723.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928131603607343.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928131603607343.png
new file mode 100644
index 0000000..34a7c50
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928131603607343.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132037622315.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132037622315.png
new file mode 100644
index 0000000..b70ab8b
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132037622315.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132120113995.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132120113995.png
new file mode 100644
index 0000000..e3b9752
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132120113995.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132208350103.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132208350103.png
new file mode 100644
index 0000000..26d845b
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132208350103.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132341491097.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132341491097.png
new file mode 100644
index 0000000..704aa82
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132341491097.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132643831562.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132643831562.png
new file mode 100644
index 0000000..c8b4b60
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132643831562.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132748009220.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132748009220.png
new file mode 100644
index 0000000..56ef422
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928132748009220.png differ
diff --git a/upload/detect/192.168.110.31/yolo/2025/09/28/20250928133042793623.png b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928133042793623.png
new file mode 100644
index 0000000..5c159ec
Binary files /dev/null and b/upload/detect/192.168.110.31/yolo/2025/09/28/20250928133042793623.png differ
diff --git a/upload/detect/192.168.110.48/face/2025/09/28/20250928141751941834.png b/upload/detect/192.168.110.48/face/2025/09/28/20250928141751941834.png
new file mode 100644
index 0000000..bf7eff1
Binary files /dev/null and b/upload/detect/192.168.110.48/face/2025/09/28/20250928141751941834.png differ
diff --git a/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135844978999.png b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135844978999.png
new file mode 100644
index 0000000..fb57046
Binary files /dev/null and b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135844978999.png differ
diff --git a/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135903804730.png b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135903804730.png
new file mode 100644
index 0000000..e9a68eb
Binary files /dev/null and b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135903804730.png differ
diff --git a/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135912802283.png b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135912802283.png
new file mode 100644
index 0000000..561e33f
Binary files /dev/null and b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135912802283.png differ
diff --git a/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135920584080.png b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135920584080.png
new file mode 100644
index 0000000..82ebe77
Binary files /dev/null and b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928135920584080.png differ
diff --git a/upload/detect/192.168.110.48/ocr/2025/09/28/20250928140614203387.png b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928140614203387.png
new file mode 100644
index 0000000..d6cae1d
Binary files /dev/null and b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928140614203387.png differ
diff --git a/upload/detect/192.168.110.48/ocr/2025/09/28/20250928141648216004.png b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928141648216004.png
new file mode 100644
index 0000000..2b4c2ac
Binary files /dev/null and b/upload/detect/192.168.110.48/ocr/2025/09/28/20250928141648216004.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928135751801387.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928135751801387.png
new file mode 100644
index 0000000..9cbe43a
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928135751801387.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928141914356832.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928141914356832.png
new file mode 100644
index 0000000..2d49b7d
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928141914356832.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928141954798783.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928141954798783.png
new file mode 100644
index 0000000..2d49b7d
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928141954798783.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142019857749.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142019857749.png
new file mode 100644
index 0000000..1653fba
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142019857749.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142148300734.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142148300734.png
new file mode 100644
index 0000000..4bb3d06
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142148300734.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142203904754.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142203904754.png
new file mode 100644
index 0000000..144f280
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142203904754.png differ
diff --git a/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142221149315.png b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142221149315.png
new file mode 100644
index 0000000..71e8cd6
Binary files /dev/null and b/upload/detect/192.168.110.48/yolo/2025/09/28/20250928142221149315.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181548659479.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181548659479.png
new file mode 100644
index 0000000..9447c50
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181548659479.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181612048245.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181612048245.png
new file mode 100644
index 0000000..d374b07
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181612048245.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181619490987.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181619490987.png
new file mode 100644
index 0000000..e0089c1
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181619490987.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181629054316.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181629054316.png
new file mode 100644
index 0000000..912b8e3
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181629054316.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181636765974.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181636765974.png
new file mode 100644
index 0000000..c8a25a6
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181636765974.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181701919246.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181701919246.png
new file mode 100644
index 0000000..3aa6e9d
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181701919246.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181735957354.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181735957354.png
new file mode 100644
index 0000000..104fefb
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181735957354.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181744977905.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181744977905.png
new file mode 100644
index 0000000..cfb44b5
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181744977905.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181748591874.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181748591874.png
new file mode 100644
index 0000000..96601fb
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181748591874.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181849437248.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181849437248.png
new file mode 100644
index 0000000..df811fd
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181849437248.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181900938365.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181900938365.png
new file mode 100644
index 0000000..ccac3be
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181900938365.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181907993814.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181907993814.png
new file mode 100644
index 0000000..213cf67
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181907993814.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181915744821.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181915744821.png
new file mode 100644
index 0000000..25a91c0
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181915744821.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181922834817.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181922834817.png
new file mode 100644
index 0000000..f64d463
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181922834817.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181941779676.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181941779676.png
new file mode 100644
index 0000000..61d4e41
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181941779676.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926181956357483.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926181956357483.png
new file mode 100644
index 0000000..b65f7c9
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926181956357483.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926182004753553.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926182004753553.png
new file mode 100644
index 0000000..a813cf9
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926182004753553.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926182107325142.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926182107325142.png
new file mode 100644
index 0000000..775229e
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926182107325142.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926183221098099.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926183221098099.png
new file mode 100644
index 0000000..9feedfc
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926183221098099.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926183226683054.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926183226683054.png
new file mode 100644
index 0000000..c320470
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926183226683054.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926183509619393.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926183509619393.png
new file mode 100644
index 0000000..e7d73d5
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926183509619393.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926183515385858.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926183515385858.png
new file mode 100644
index 0000000..6c6f0fb
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926183515385858.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926183549559005.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926183549559005.png
new file mode 100644
index 0000000..dc692a4
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926183549559005.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926184033302428.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926184033302428.png
new file mode 100644
index 0000000..c3d7177
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926184033302428.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926184202262753.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926184202262753.png
new file mode 100644
index 0000000..b4cda42
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926184202262753.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926184225940468.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926184225940468.png
new file mode 100644
index 0000000..179b040
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926184225940468.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926184237962425.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926184237962425.png
new file mode 100644
index 0000000..2c87698
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926184237962425.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926184612765839.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926184612765839.png
new file mode 100644
index 0000000..28f1c3c
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926184612765839.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926184721713634.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926184721713634.png
new file mode 100644
index 0000000..660dcf6
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926184721713634.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926185107990894.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926185107990894.png
new file mode 100644
index 0000000..ee0574c
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926185107990894.png differ
diff --git a/upload/detect/192.168.110.71/face/2025/09/26/20250926185438332779.png b/upload/detect/192.168.110.71/face/2025/09/26/20250926185438332779.png
new file mode 100644
index 0000000..72cde52
Binary files /dev/null and b/upload/detect/192.168.110.71/face/2025/09/26/20250926185438332779.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926182051687491.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926182051687491.png
new file mode 100644
index 0000000..708b270
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926182051687491.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926182152738779.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926182152738779.png
new file mode 100644
index 0000000..551be55
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926182152738779.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183137658781.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183137658781.png
new file mode 100644
index 0000000..e9508d0
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183137658781.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183338440997.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183338440997.png
new file mode 100644
index 0000000..3e33542
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183338440997.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183413689093.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183413689093.png
new file mode 100644
index 0000000..7ef587f
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183413689093.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183427508986.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183427508986.png
new file mode 100644
index 0000000..52be948
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183427508986.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183445803425.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183445803425.png
new file mode 100644
index 0000000..0f4dc98
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926183445803425.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926184052202422.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926184052202422.png
new file mode 100644
index 0000000..e465499
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926184052202422.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185309105562.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185309105562.png
new file mode 100644
index 0000000..b46e5e1
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185309105562.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185335145854.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185335145854.png
new file mode 100644
index 0000000..136cd49
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185335145854.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185401908188.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185401908188.png
new file mode 100644
index 0000000..0237f5f
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185401908188.png differ
diff --git a/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185403839784.png b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185403839784.png
new file mode 100644
index 0000000..27303e7
Binary files /dev/null and b/upload/detect/192.168.110.71/ocr/2025/09/26/20250926185403839784.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183311544782.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183311544782.png
new file mode 100644
index 0000000..51deef4
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183311544782.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183317217191.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183317217191.png
new file mode 100644
index 0000000..12cbfb9
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183317217191.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183321586591.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183321586591.png
new file mode 100644
index 0000000..9e8b3a0
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183321586591.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183326276146.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183326276146.png
new file mode 100644
index 0000000..13303c9
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183326276146.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183608497282.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183608497282.png
new file mode 100644
index 0000000..ef3946a
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183608497282.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183613653991.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183613653991.png
new file mode 100644
index 0000000..98e9008
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183613653991.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183629980056.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183629980056.png
new file mode 100644
index 0000000..2456f55
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183629980056.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183634357811.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183634357811.png
new file mode 100644
index 0000000..4d0a36d
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183634357811.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183656479247.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183656479247.png
new file mode 100644
index 0000000..78d8af6
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183656479247.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183700800685.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183700800685.png
new file mode 100644
index 0000000..2690713
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183700800685.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183713348851.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183713348851.png
new file mode 100644
index 0000000..26d61ec
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183713348851.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183719652510.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183719652510.png
new file mode 100644
index 0000000..8a3484e
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183719652510.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183737812595.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183737812595.png
new file mode 100644
index 0000000..700d217
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926183737812595.png differ
diff --git a/upload/detect/192.168.110.71/yolo/2025/09/26/20250926185155061793.png b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926185155061793.png
new file mode 100644
index 0000000..97e5a17
Binary files /dev/null and b/upload/detect/192.168.110.71/yolo/2025/09/26/20250926185155061793.png differ
diff --git a/upload/source/face/2025/09/23/20250923160041747121_111.jpg b/upload/source/face/2025/09/23/20250923160041747121_111.jpg
new file mode 100644
index 0000000..b84eac6
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160041747121_111.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160044340530_20160609205509_GhTNF.jpeg b/upload/source/face/2025/09/23/20250923160044340530_20160609205509_GhTNF.jpeg
new file mode 100644
index 0000000..289cb51
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160044340530_20160609205509_GhTNF.jpeg differ
diff --git a/upload/source/face/2025/09/23/20250923160044681195_20220124225439_4c16c.jpg b/upload/source/face/2025/09/23/20250923160044681195_20220124225439_4c16c.jpg
new file mode 100644
index 0000000..ca01255
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160044681195_20220124225439_4c16c.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160045599006_20220302214915_d6323.jpg b/upload/source/face/2025/09/23/20250923160045599006_20220302214915_d6323.jpg
new file mode 100644
index 0000000..94fcd21
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160045599006_20220302214915_d6323.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160045967129_300.jpg b/upload/source/face/2025/09/23/20250923160045967129_300.jpg
new file mode 100644
index 0000000..96c4aa0
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160045967129_300.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160046299811_R-C.jpg b/upload/source/face/2025/09/23/20250923160046299811_R-C.jpg
new file mode 100644
index 0000000..5eb1cf6
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160046299811_R-C.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160106553154_111.jpg b/upload/source/face/2025/09/23/20250923160106553154_111.jpg
new file mode 100644
index 0000000..b84eac6
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160106553154_111.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160106945409_20160609205509_GhTNF.jpeg b/upload/source/face/2025/09/23/20250923160106945409_20160609205509_GhTNF.jpeg
new file mode 100644
index 0000000..289cb51
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160106945409_20160609205509_GhTNF.jpeg differ
diff --git a/upload/source/face/2025/09/23/20250923160107324588_20220124225439_4c16c.jpg b/upload/source/face/2025/09/23/20250923160107324588_20220124225439_4c16c.jpg
new file mode 100644
index 0000000..ca01255
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160107324588_20220124225439_4c16c.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160107682577_20220302214915_d6323.jpg b/upload/source/face/2025/09/23/20250923160107682577_20220302214915_d6323.jpg
new file mode 100644
index 0000000..94fcd21
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160107682577_20220302214915_d6323.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160108037669_300.jpg b/upload/source/face/2025/09/23/20250923160108037669_300.jpg
new file mode 100644
index 0000000..96c4aa0
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160108037669_300.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923160108363329_R-C.jpg b/upload/source/face/2025/09/23/20250923160108363329_R-C.jpg
new file mode 100644
index 0000000..5eb1cf6
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923160108363329_R-C.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161335443331_丁仲礼.jpg b/upload/source/face/2025/09/23/20250923161335443331_丁仲礼.jpg
new file mode 100644
index 0000000..25c3e71
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161335443331_丁仲礼.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161335801448_丁薛祥.jpg b/upload/source/face/2025/09/23/20250923161335801448_丁薛祥.jpg
new file mode 100644
index 0000000..5d4dd25
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161335801448_丁薛祥.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161336189527_万立骏.jpg b/upload/source/face/2025/09/23/20250923161336189527_万立骏.jpg
new file mode 100644
index 0000000..75cb796
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161336189527_万立骏.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161336623652_万钢.jpg b/upload/source/face/2025/09/23/20250923161336623652_万钢.jpg
new file mode 100644
index 0000000..c44546f
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161336623652_万钢.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161337011169_习近平.jpg b/upload/source/face/2025/09/23/20250923161337011169_习近平.jpg
new file mode 100644
index 0000000..ee5f7c8
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161337011169_习近平.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161337325711_任鸿斌.jpg b/upload/source/face/2025/09/23/20250923161337325711_任鸿斌.jpg
new file mode 100644
index 0000000..980b15c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161337325711_任鸿斌.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161337670071_何卫东.jpg b/upload/source/face/2025/09/23/20250923161337670071_何卫东.jpg
new file mode 100644
index 0000000..289c59e
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161337670071_何卫东.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161338041821_何厚铧.jpg b/upload/source/face/2025/09/23/20250923161338041821_何厚铧.jpg
new file mode 100644
index 0000000..e8d8b83
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161338041821_何厚铧.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161338385445_何平.jpg b/upload/source/face/2025/09/23/20250923161338385445_何平.jpg
new file mode 100644
index 0000000..32689d7
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161338385445_何平.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161338726226_何报翔.jpg b/upload/source/face/2025/09/23/20250923161338726226_何报翔.jpg
new file mode 100644
index 0000000..edcfce8
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161338726226_何报翔.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161339043277_何立峰.jpg b/upload/source/face/2025/09/23/20250923161339043277_何立峰.jpg
new file mode 100644
index 0000000..f929a6a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161339043277_何立峰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161339369377_何维.jpg b/upload/source/face/2025/09/23/20250923161339369377_何维.jpg
new file mode 100644
index 0000000..b7d4b2b
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161339369377_何维.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161339831842_侯凯.jpg b/upload/source/face/2025/09/23/20250923161339831842_侯凯.jpg
new file mode 100644
index 0000000..db5544c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161339831842_侯凯.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161340161394_倪虹.jpeg b/upload/source/face/2025/09/23/20250923161340161394_倪虹.jpeg
new file mode 100644
index 0000000..7e426c6
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161340161394_倪虹.jpeg differ
diff --git a/upload/source/face/2025/09/23/20250923161340546942_关志鸥.jpg b/upload/source/face/2025/09/23/20250923161340546942_关志鸥.jpg
new file mode 100644
index 0000000..418a843
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161340546942_关志鸥.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161340870026_刘伟.jpg b/upload/source/face/2025/09/23/20250923161340870026_刘伟.jpg
new file mode 100644
index 0000000..5e890d0
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161340870026_刘伟.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161341310447_刘国中.jpg b/upload/source/face/2025/09/23/20250923161341310447_刘国中.jpg
new file mode 100644
index 0000000..f9876e5
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161341310447_刘国中.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161341638230_刘奇.jpg b/upload/source/face/2025/09/23/20250923161341638230_刘奇.jpg
new file mode 100644
index 0000000..849dc87
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161341638230_刘奇.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161341992144_刘金国.jpg b/upload/source/face/2025/09/23/20250923161341992144_刘金国.jpg
new file mode 100644
index 0000000..ea4d66e
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161341992144_刘金国.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161342357339_吴政隆.jpg b/upload/source/face/2025/09/23/20250923161342357339_吴政隆.jpg
new file mode 100644
index 0000000..86e1452
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161342357339_吴政隆.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161342734137_周强.jpg b/upload/source/face/2025/09/23/20250923161342734137_周强.jpg
new file mode 100644
index 0000000..526a449
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161342734137_周强.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161343096749_咸辉.jpg b/upload/source/face/2025/09/23/20250923161343096749_咸辉.jpg
new file mode 100644
index 0000000..cfc7af8
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161343096749_咸辉.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161343456166_姜信治.jpg b/upload/source/face/2025/09/23/20250923161343456166_姜信治.jpg
new file mode 100644
index 0000000..9fcf67a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161343456166_姜信治.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161343872199_孙业礼.jpg b/upload/source/face/2025/09/23/20250923161343872199_孙业礼.jpg
new file mode 100644
index 0000000..8e9684d
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161343872199_孙业礼.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161344287557_尹力.jpg b/upload/source/face/2025/09/23/20250923161344287557_尹力.jpg
new file mode 100644
index 0000000..3abb1b7
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161344287557_尹力.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161344659184_巴特尔.jpg b/upload/source/face/2025/09/23/20250923161344659184_巴特尔.jpg
new file mode 100644
index 0000000..d78c863
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161344659184_巴特尔.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161345031761_帕巴拉·格列朗杰.jpg b/upload/source/face/2025/09/23/20250923161345031761_帕巴拉·格列朗杰.jpg
new file mode 100644
index 0000000..b7cad92
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161345031761_帕巴拉·格列朗杰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161345375273_应勇.jpg b/upload/source/face/2025/09/23/20250923161345375273_应勇.jpg
new file mode 100644
index 0000000..a084d61
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161345375273_应勇.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161345749915_张军.jpg b/upload/source/face/2025/09/23/20250923161345749915_张军.jpg
new file mode 100644
index 0000000..7eafb6a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161345749915_张军.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161346137920_张又侠.jpg b/upload/source/face/2025/09/23/20250923161346137920_张又侠.jpg
new file mode 100644
index 0000000..aed4d6d
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161346137920_张又侠.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161346484327_张国清.jpg b/upload/source/face/2025/09/23/20250923161346484327_张国清.jpg
new file mode 100644
index 0000000..a54c65b
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161346484327_张国清.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161346843386_张宏森.jpg b/upload/source/face/2025/09/23/20250923161346843386_张宏森.jpg
new file mode 100644
index 0000000..3e599ea
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161346843386_张宏森.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161347254496_张庆伟.jpg b/upload/source/face/2025/09/23/20250923161347254496_张庆伟.jpg
new file mode 100644
index 0000000..e414d76
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161347254496_张庆伟.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161347568131_彭清华.jpg b/upload/source/face/2025/09/23/20250923161347568131_彭清华.jpg
new file mode 100644
index 0000000..a7dd22c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161347568131_彭清华.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161348009832_怀进鹏.jpg b/upload/source/face/2025/09/23/20250923161348009832_怀进鹏.jpg
new file mode 100644
index 0000000..a4b768d
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161348009832_怀进鹏.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161348334567_朱永新.jpg b/upload/source/face/2025/09/23/20250923161348334567_朱永新.jpg
new file mode 100644
index 0000000..4e2067d
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161348334567_朱永新.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161348707537_李乐成.png b/upload/source/face/2025/09/23/20250923161348707537_李乐成.png
new file mode 100644
index 0000000..0b7609c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161348707537_李乐成.png differ
diff --git a/upload/source/face/2025/09/23/20250923161349169451_李书磊.jpg b/upload/source/face/2025/09/23/20250923161349169451_李书磊.jpg
new file mode 100644
index 0000000..39dfa2b
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161349169451_李书磊.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161349652454_李国英.jpg b/upload/source/face/2025/09/23/20250923161349652454_李国英.jpg
new file mode 100644
index 0000000..2af6807
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161349652454_李国英.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161350144090_李希.jpg b/upload/source/face/2025/09/23/20250923161350144090_李希.jpg
new file mode 100644
index 0000000..104767a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161350144090_李希.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161350669040_李干杰.jpg b/upload/source/face/2025/09/23/20250923161350669040_李干杰.jpg
new file mode 100644
index 0000000..d074b92
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161350669040_李干杰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161350980039_李强.jpg b/upload/source/face/2025/09/23/20250923161350980039_李强.jpg
new file mode 100644
index 0000000..67dfe18
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161350980039_李强.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161351462444_李斌.jpg b/upload/source/face/2025/09/23/20250923161351462444_李斌.jpg
new file mode 100644
index 0000000..540a474
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161351462444_李斌.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161351804536_李鸿忠.jpg b/upload/source/face/2025/09/23/20250923161351804536_李鸿忠.jpg
new file mode 100644
index 0000000..7faaf30
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161351804536_李鸿忠.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161352916115_杨万明.png b/upload/source/face/2025/09/23/20250923161352916115_杨万明.png
new file mode 100644
index 0000000..2664040
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161352916115_杨万明.png differ
diff --git a/upload/source/face/2025/09/23/20250923161353279182_杨震.jpg b/upload/source/face/2025/09/23/20250923161353279182_杨震.jpg
new file mode 100644
index 0000000..56943bd
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161353279182_杨震.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161353658693_林上元.jpg b/upload/source/face/2025/09/23/20250923161353658693_林上元.jpg
new file mode 100644
index 0000000..f022699
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161353658693_林上元.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161354134258_梁振英.jpg b/upload/source/face/2025/09/23/20250923161354134258_梁振英.jpg
new file mode 100644
index 0000000..04d1829
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161354134258_梁振英.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161354496999_武维华.jpg b/upload/source/face/2025/09/23/20250923161354496999_武维华.jpg
new file mode 100644
index 0000000..bc2b0d4
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161354496999_武维华.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161354855685_沈跃跃.jpg b/upload/source/face/2025/09/23/20250923161354855685_沈跃跃.jpg
new file mode 100644
index 0000000..f12bc05
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161354855685_沈跃跃.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161355292694_洛桑江村.jpg b/upload/source/face/2025/09/23/20250923161355292694_洛桑江村.jpg
new file mode 100644
index 0000000..73161dc
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161355292694_洛桑江村.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161355666255_潘功胜.jpg b/upload/source/face/2025/09/23/20250923161355666255_潘功胜.jpg
new file mode 100644
index 0000000..d37cea8
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161355666255_潘功胜.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161356048827_王东峰.jpg b/upload/source/face/2025/09/23/20250923161356048827_王东峰.jpg
new file mode 100644
index 0000000..9520532
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161356048827_王东峰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161356357156_王东明.jpg b/upload/source/face/2025/09/23/20250923161356357156_王东明.jpg
new file mode 100644
index 0000000..15c7023
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161356357156_王东明.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161356796715_王光谦.jpg b/upload/source/face/2025/09/23/20250923161356796715_王光谦.jpg
new file mode 100644
index 0000000..b4671a7
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161356796715_王光谦.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161357168730_王刚.jpg b/upload/source/face/2025/09/23/20250923161357168730_王刚.jpg
new file mode 100644
index 0000000..9401343
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161357168730_王刚.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161357560673_王勇.jpg b/upload/source/face/2025/09/23/20250923161357560673_王勇.jpg
new file mode 100644
index 0000000..f73497a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161357560673_王勇.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161357900373_王小洪.jpg b/upload/source/face/2025/09/23/20250923161357900373_王小洪.jpg
new file mode 100644
index 0000000..52b4d3d
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161357900373_王小洪.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161358313763_王文涛.jpg b/upload/source/face/2025/09/23/20250923161358313763_王文涛.jpg
new file mode 100644
index 0000000..2b50bcf
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161358313763_王文涛.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161358668346_王晓萍.jpg b/upload/source/face/2025/09/23/20250923161358668346_王晓萍.jpg
new file mode 100644
index 0000000..e8ad01f
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161358668346_王晓萍.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161359071890_王毅.jpg b/upload/source/face/2025/09/23/20250923161359071890_王毅.jpg
new file mode 100644
index 0000000..7fed061
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161359071890_王毅.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161359450966_王沪宁.jpg b/upload/source/face/2025/09/23/20250923161359450966_王沪宁.jpg
new file mode 100644
index 0000000..00fb732
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161359450966_王沪宁.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161359958446_王祥喜.jpg b/upload/source/face/2025/09/23/20250923161359958446_王祥喜.jpg
new file mode 100644
index 0000000..2936e4c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161359958446_王祥喜.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161400411901_王超.jpg b/upload/source/face/2025/09/23/20250923161400411901_王超.jpg
new file mode 100644
index 0000000..f0afa15
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161400411901_王超.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161400911160_石泰峰.jpg b/upload/source/face/2025/09/23/20250923161400911160_石泰峰.jpg
new file mode 100644
index 0000000..18b2010
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161400911160_石泰峰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161401297262_秦博勇.jpg b/upload/source/face/2025/09/23/20250923161401297262_秦博勇.jpg
new file mode 100644
index 0000000..18c922a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161401297262_秦博勇.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161402053989_程凯.jpg b/upload/source/face/2025/09/23/20250923161402053989_程凯.jpg
new file mode 100644
index 0000000..f730399
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161402053989_程凯.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161402447035_穆虹.jpg b/upload/source/face/2025/09/23/20250923161402447035_穆虹.jpg
new file mode 100644
index 0000000..887a6b4
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161402447035_穆虹.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161402821593_肖捷.jpg b/upload/source/face/2025/09/23/20250923161402821593_肖捷.jpg
new file mode 100644
index 0000000..6d2ee82
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161402821593_肖捷.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161403196336_胡和平.jpg b/upload/source/face/2025/09/23/20250923161403196336_胡和平.jpg
new file mode 100644
index 0000000..9776f3a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161403196336_胡和平.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161403538787_胡春华.jpg b/upload/source/face/2025/09/23/20250923161403538787_胡春华.jpg
new file mode 100644
index 0000000..a7596b4
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161403538787_胡春华.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161404015038_苏辉.jpg b/upload/source/face/2025/09/23/20250923161404015038_苏辉.jpg
new file mode 100644
index 0000000..9085429
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161404015038_苏辉.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161404488715_蒋作君.jpg b/upload/source/face/2025/09/23/20250923161404488715_蒋作君.jpg
new file mode 100644
index 0000000..2edd590
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161404488715_蒋作君.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161405139450_蓝佛安.jpg b/upload/source/face/2025/09/23/20250923161405139450_蓝佛安.jpg
new file mode 100644
index 0000000..0be25d0
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161405139450_蓝佛安.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161405700327_蔡奇.jpg b/upload/source/face/2025/09/23/20250923161405700327_蔡奇.jpg
new file mode 100644
index 0000000..1cec9cc
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161405700327_蔡奇.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161406031992_蔡达峰.jpg b/upload/source/face/2025/09/23/20250923161406031992_蔡达峰.jpg
new file mode 100644
index 0000000..f0a4e00
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161406031992_蔡达峰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161406433744_袁家军.jpg b/upload/source/face/2025/09/23/20250923161406433744_袁家军.jpg
new file mode 100644
index 0000000..fb79c61
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161406433744_袁家军.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161406782185_裴金佳.jpeg b/upload/source/face/2025/09/23/20250923161406782185_裴金佳.jpeg
new file mode 100644
index 0000000..0497988
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161406782185_裴金佳.jpeg differ
diff --git a/upload/source/face/2025/09/23/20250923161407221070_谌贻琴.jpg b/upload/source/face/2025/09/23/20250923161407221070_谌贻琴.jpg
new file mode 100644
index 0000000..15c52e7
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161407221070_谌贻琴.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161407591502_贺荣.jpg b/upload/source/face/2025/09/23/20250923161407591502_贺荣.jpg
new file mode 100644
index 0000000..a41cfdf
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161407591502_贺荣.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161408009256_赵乐际.jpg b/upload/source/face/2025/09/23/20250923161408009256_赵乐际.jpg
new file mode 100644
index 0000000..ed34360
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161408009256_赵乐际.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161408333312_邵鸿.jpg b/upload/source/face/2025/09/23/20250923161408333312_邵鸿.jpg
new file mode 100644
index 0000000..1af274a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161408333312_邵鸿.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161408693544_郑建邦.jpg b/upload/source/face/2025/09/23/20250923161408693544_郑建邦.jpg
new file mode 100644
index 0000000..92dbfb4
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161408693544_郑建邦.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161409088071_郑建闽.jpg b/upload/source/face/2025/09/23/20250923161409088071_郑建闽.jpg
new file mode 100644
index 0000000..122ba02
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161409088071_郑建闽.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161409508997_郑栅洁.jpg b/upload/source/face/2025/09/23/20250923161409508997_郑栅洁.jpg
new file mode 100644
index 0000000..2ef6098
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161409508997_郑栅洁.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161409851414_郝明金.jpg b/upload/source/face/2025/09/23/20250923161409851414_郝明金.jpg
new file mode 100644
index 0000000..17a82df
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161409851414_郝明金.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161410160916_铁凝.jpg b/upload/source/face/2025/09/23/20250923161410160916_铁凝.jpg
new file mode 100644
index 0000000..439832c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161410160916_铁凝.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161410522396_阴和俊.jpg b/upload/source/face/2025/09/23/20250923161410522396_阴和俊.jpg
new file mode 100644
index 0000000..d69a48c
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161410522396_阴和俊.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161410871265_阿东.jpg b/upload/source/face/2025/09/23/20250923161410871265_阿东.jpg
new file mode 100644
index 0000000..504b868
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161410871265_阿东.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161411244093_陆治原.jpg b/upload/source/face/2025/09/23/20250923161411244093_陆治原.jpg
new file mode 100644
index 0000000..7248913
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161411244093_陆治原.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161411587887_陈一新.jpg b/upload/source/face/2025/09/23/20250923161411587887_陈一新.jpg
new file mode 100644
index 0000000..5b4be88
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161411587887_陈一新.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161411903887_陈吉宁.jpg b/upload/source/face/2025/09/23/20250923161411903887_陈吉宁.jpg
new file mode 100644
index 0000000..3dcbae7
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161411903887_陈吉宁.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161412278186_陈敏尔.jpg b/upload/source/face/2025/09/23/20250923161412278186_陈敏尔.jpg
new file mode 100644
index 0000000..d23b07a
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161412278186_陈敏尔.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161412687593_陈文清.jpg b/upload/source/face/2025/09/23/20250923161412687593_陈文清.jpg
new file mode 100644
index 0000000..bdb680f
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161412687593_陈文清.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161413028586_陈文清.png b/upload/source/face/2025/09/23/20250923161413028586_陈文清.png
new file mode 100644
index 0000000..efb1bb2
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161413028586_陈文清.png differ
diff --git a/upload/source/face/2025/09/23/20250923161413328477_陈武.jpg b/upload/source/face/2025/09/23/20250923161413328477_陈武.jpg
new file mode 100644
index 0000000..8048bf9
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161413328477_陈武.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161413690517_陈瑞峰.jpg b/upload/source/face/2025/09/23/20250923161413690517_陈瑞峰.jpg
new file mode 100644
index 0000000..ff23e06
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161413690517_陈瑞峰.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161414079008_雪克来提·扎克尔.jpg b/upload/source/face/2025/09/23/20250923161414079008_雪克来提·扎克尔.jpg
new file mode 100644
index 0000000..1d9be1b
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161414079008_雪克来提·扎克尔.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161414656980_雷海潮.jpg b/upload/source/face/2025/09/23/20250923161414656980_雷海潮.jpg
new file mode 100644
index 0000000..81dde17
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161414656980_雷海潮.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161415400502_韩俊.jpg b/upload/source/face/2025/09/23/20250923161415400502_韩俊.jpg
new file mode 100644
index 0000000..db67aed
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161415400502_韩俊.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161416111345_韩正.jpg b/upload/source/face/2025/09/23/20250923161416111345_韩正.jpg
new file mode 100644
index 0000000..7366172
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161416111345_韩正.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161416490506_马兴瑞.jpg b/upload/source/face/2025/09/23/20250923161416490506_马兴瑞.jpg
new file mode 100644
index 0000000..b4664cc
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161416490506_马兴瑞.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161417166178_高云龙.jpg b/upload/source/face/2025/09/23/20250923161417166178_高云龙.jpg
new file mode 100644
index 0000000..4469ebc
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161417166178_高云龙.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161417836475_黄坤明.jpg b/upload/source/face/2025/09/23/20250923161417836475_黄坤明.jpg
new file mode 100644
index 0000000..42cf924
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161417836475_黄坤明.jpg differ
diff --git a/upload/source/face/2025/09/23/20250923161418532804_黄润秋.jpg b/upload/source/face/2025/09/23/20250923161418532804_黄润秋.jpg
new file mode 100644
index 0000000..050549e
Binary files /dev/null and b/upload/source/face/2025/09/23/20250923161418532804_黄润秋.jpg differ
diff --git a/upload/source/model/2025/09/18/best.pt b/upload/source/model/2025/09/18/best.pt
new file mode 100644
index 0000000..6fc42cc
Binary files /dev/null and b/upload/source/model/2025/09/18/best.pt differ
diff --git a/upload/source/model/2025/09/18/last.pt b/upload/source/model/2025/09/18/last.pt
new file mode 100644
index 0000000..e69de29
diff --git a/upload/source/model/2025/09/18/yolo11n.pt b/upload/source/model/2025/09/18/yolo11n.pt
new file mode 100644
index 0000000..45b273b
Binary files /dev/null and b/upload/source/model/2025/09/18/yolo11n.pt differ
diff --git a/upload/source/model/2025/09/19/20250919101808847293_best.pt b/upload/source/model/2025/09/19/20250919101808847293_best.pt
new file mode 100644
index 0000000..8c54c39
Binary files /dev/null and b/upload/source/model/2025/09/19/20250919101808847293_best.pt differ
diff --git a/upload/source/model/2025/09/19/20250919101846261213_best.pt b/upload/source/model/2025/09/19/20250919101846261213_best.pt
new file mode 100644
index 0000000..8c54c39
Binary files /dev/null and b/upload/source/model/2025/09/19/20250919101846261213_best.pt differ
diff --git a/upload/source/model/2025/09/19/best.pt b/upload/source/model/2025/09/19/best.pt
new file mode 100644
index 0000000..8c54c39
Binary files /dev/null and b/upload/source/model/2025/09/19/best.pt differ
diff --git a/upload/source/model/2025/09/22/20250922154252490696_best.pt b/upload/source/model/2025/09/22/20250922154252490696_best.pt
new file mode 100644
index 0000000..c930379
Binary files /dev/null and b/upload/source/model/2025/09/22/20250922154252490696_best.pt differ
diff --git a/upload/source/model/2025/09/22/20250922154451027461_best(1).pt b/upload/source/model/2025/09/22/20250922154451027461_best(1).pt
new file mode 100644
index 0000000..c930379
Binary files /dev/null and b/upload/source/model/2025/09/22/20250922154451027461_best(1).pt differ
diff --git a/upload/source/model/2025/09/28/20250928124154611052_best.pt b/upload/source/model/2025/09/28/20250928124154611052_best.pt
new file mode 100644
index 0000000..15566d4
Binary files /dev/null and b/upload/source/model/2025/09/28/20250928124154611052_best.pt differ
diff --git a/util/__pycache__/face_util.cpython-310.pyc b/util/__pycache__/face_util.cpython-310.pyc
new file mode 100644
index 0000000..2c89e2d
Binary files /dev/null and b/util/__pycache__/face_util.cpython-310.pyc differ
diff --git a/util/face_util.py b/util/face_util.py
new file mode 100644
index 0000000..1ae17a7
--- /dev/null
+++ b/util/face_util.py
@@ -0,0 +1,196 @@
+import cv2
+import numpy as np
+import insightface
+from insightface.app import FaceAnalysis
+from io import BytesIO
+from PIL import Image
+import logging
+from mysql.connector import Error as MySQLError
+
+from ds.db import db
+
+# 配置日志(便于排查)
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - [FaceUtil] - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+# 全局变量存储InsightFace引擎和特征列表
+_insightface_app = None
+_feature_list = []
+
+
+def init_insightface():
+ """初始化InsightFace引擎(确保成功后再使用)"""
+ global _insightface_app
+ try:
+ if _insightface_app is not None:
+ logger.info("InsightFace引擎已初始化、无需重复执行")
+ return _insightface_app
+
+ logger.info("正在初始化InsightFace引擎(模型:buffalo_l)...")
+ # 手动指定模型下载路径(避免权限问题、可选)
+ app = FaceAnalysis(
+ name='buffalo_l',
+ root='~/.insightface', # 模型默认下载路径
+ providers=['CPUExecutionProvider'] # 强制用CPU(若有GPU可加'CUDAExecutionProvider')
+ )
+ app.prepare(ctx_id=0, det_size=(640, 640)) # det_size越大、小人脸检测越准
+ logger.info("InsightFace引擎初始化完成")
+ _insightface_app = app
+ return app
+ except Exception as e:
+ logger.error(f"InsightFace初始化失败:{str(e)}", exc_info=True) # 打印详细堆栈
+ _insightface_app = None
+ return None
+
+
+def add_binary_data(binary_data):
+ """
+ 接收单张图片的二进制数据、提取特征并保存
+ 返回:(True, 特征值numpy数组) 或 (False, 错误信息字符串)
+ """
+ global _insightface_app, _feature_list
+
+ # 1. 先检查引擎是否初始化成功
+ if not _insightface_app:
+ init_result = init_insightface() # 尝试重新初始化
+ if not init_result:
+ error_msg = "InsightFace引擎未初始化、无法检测人脸"
+ logger.error(error_msg)
+ return False, error_msg
+
+ try:
+ # 2. 验证二进制数据有效性
+ if len(binary_data) < 1024: # 过滤过小的无效图片(小于1KB)
+ error_msg = f"图片过小({len(binary_data)}字节)、可能不是有效图片"
+ logger.warning(error_msg)
+ return False, error_msg
+
+ # 3. 二进制数据转CV2格式(关键步骤、避免通道错误)
+ try:
+ img = Image.open(BytesIO(binary_data)).convert("RGB") # 强制转RGB
+ frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) # InsightFace需要BGR格式
+ except Exception as e:
+ error_msg = f"图片格式转换失败:{str(e)}"
+ logger.error(error_msg, exc_info=True)
+ return False, error_msg
+
+ # 4. 检查图片尺寸(避免极端尺寸导致检测失败)
+ height, width = frame.shape[:2]
+ if height < 64 or width < 64: # 人脸检测最小建议尺寸
+ error_msg = f"图片尺寸过小({width}x{height})、需至少64x64像素"
+ logger.warning(error_msg)
+ return False, error_msg
+
+ # 5. 调用InsightFace检测人脸
+ logger.info(f"开始检测人脸(图片尺寸:{width}x{height}、格式:BGR)")
+ faces = _insightface_app.get(frame)
+
+ if not faces:
+ error_msg = "未检测到人脸(请确保图片包含清晰正面人脸、无遮挡、不模糊)"
+ logger.warning(error_msg)
+ return False, error_msg
+
+ # 6. 提取特征并保存
+ current_feature = faces[0].embedding
+ _feature_list.append(current_feature)
+ logger.info(f"人脸检测成功、提取特征值(维度:{current_feature.shape[0]})、累计特征数:{len(_feature_list)}")
+ return True, current_feature
+
+ except Exception as e:
+ error_msg = f"处理图片时发生异常:{str(e)}"
+ logger.error(error_msg, exc_info=True)
+ return False, error_msg
+
+
+# ------------------------------
+# 获取数据库最新的人脸及其特征
+# ------------------------------
+def get_all_face_name_with_eigenvalue() -> dict:
+ conn = None
+ cursor = None
+ try:
+ conn = db.get_connection()
+ cursor = conn.cursor(dictionary=True)
+
+ query = "SELECT name, eigenvalue FROM face WHERE name IS NOT NULL"
+ cursor.execute(query)
+ faces = cursor.fetchall()
+
+ name_to_eigenvalues = {}
+ for face in faces:
+ name = face["name"]
+ eigenvalue = face["eigenvalue"]
+ if name in name_to_eigenvalues:
+ name_to_eigenvalues[name].append(eigenvalue)
+ else:
+ name_to_eigenvalues[name] = [eigenvalue]
+
+ face_dict = {}
+ for name, eigenvalues in name_to_eigenvalues.items():
+ if len(eigenvalues) > 1:
+ face_dict[name] = get_average_feature(eigenvalues)
+ else:
+ face_dict[name] = eigenvalues[0]
+
+ return face_dict
+
+ except MySQLError as e:
+ raise Exception(f"获取人脸特征失败: {str(e)}") from e
+ finally:
+ db.close_connection(conn, cursor)
+
+# 以下函数保持不变
+def get_average_feature(features=None):
+ global _feature_list
+ try:
+ if features is None:
+ features = _feature_list
+ if not isinstance(features, list) or len(features) == 0:
+ logger.warning("输入必须是包含至少一个特征值的列表")
+ return None
+
+ processed_features = []
+ for i, embedding in enumerate(features):
+ try:
+ if isinstance(embedding, str):
+ embedding_str = embedding.replace('[', '').replace(']', '').replace(',', ' ').strip()
+ embedding_list = [float(num) for num in embedding_str.split() if num.strip()]
+ embedding_np = np.array(embedding_list, dtype=np.float32)
+ else:
+ embedding_np = np.array(embedding, dtype=np.float32)
+
+ if len(embedding_np.shape) == 1:
+ processed_features.append(embedding_np)
+ logger.info(f"已添加第 {i + 1} 个特征值用于计算平均值")
+ else:
+ logger.warning(f"跳过第 {i + 1} 个特征值:不是一维数组")
+ except Exception as e:
+ logger.error(f"处理第 {i + 1} 个特征值时出错:{str(e)}")
+
+ if not processed_features:
+ logger.warning("没有有效的特征值用于计算平均值")
+ return None
+
+ dims = {feat.shape[0] for feat in processed_features}
+ if len(dims) > 1:
+ logger.error(f"特征值维度不一致:{dims}、无法计算平均值")
+ return None
+
+ avg_feature = np.mean(processed_features, axis=0)
+ logger.info(f"计算成功:{len(processed_features)} 个特征值的平均向量(维度:{avg_feature.shape[0]})")
+ return avg_feature
+ except Exception as e:
+ logger.error(f"计算平均特征值出错:{str(e)}", exc_info=True)
+ return None
+
+
+def clear_features():
+ global _feature_list
+ _feature_list = []
+ logger.info("已清空所有特征数据")
+
+
+def get_feature_list():
+ global _feature_list
+ logger.info(f"当前特征列表长度:{len(_feature_list)}")
+ return _feature_list.copy()
\ No newline at end of file
diff --git a/util/model_util.py b/util/model_util.py
new file mode 100644
index 0000000..e3e999e
--- /dev/null
+++ b/util/model_util.py
@@ -0,0 +1,61 @@
+import os
+import numpy as np
+import traceback
+from ultralytics import YOLO
+from typing import Optional
+
+
+def load_yolo_model(model_path: str) -> Optional[YOLO]:
+ """
+ 加载YOLO模型(支持v5/v8)、并校验模型有效性
+ :param model_path: 模型文件的绝对路径
+ :return: 加载成功返回YOLO模型实例、失败返回None
+ """
+ try:
+ # 加载前的基础信息检查
+ print(f"\n[模型工具] 开始加载模型:{model_path}")
+ print(f"[模型工具] 文件是否存在:{os.path.exists(model_path)}")
+ if os.path.exists(model_path):
+ print(f"[模型工具] 文件大小:{os.path.getsize(model_path) / 1024 / 1024:.2f} MB")
+
+ # 强制重新加载模型、避免缓存问题
+ model = YOLO(model_path)
+
+ # 兼容性校验:使用numpy空数组测试模型
+ dummy_image = np.zeros((640, 640, 3), dtype=np.uint8)
+
+ try:
+ # 优先使用新版本参数
+ model.predict(
+ source=dummy_image,
+ imgsz=640,
+ conf=0.25,
+ verbose=False,
+ stream=False
+ )
+ except Exception as pred_e:
+ print(f"[模型工具] 预测校验兼容处理:{str(pred_e)}")
+ # 兼容旧版本YOLO参数
+ model.predict(
+ img=dummy_image,
+ imgsz=640,
+ conf=0.25,
+ verbose=False
+ )
+
+ # 验证模型基本属性
+ if not hasattr(model, 'names'):
+ print("[模型工具] 警告:模型缺少类别名称属性")
+ else:
+ print(f"[模型工具] 模型包含类别:{list(model.names.values())[:5]}...") # 显示前5个类别
+
+ print(f"[模型工具] 模型加载成功!")
+ return model
+
+ except Exception as e:
+ # 详细错误信息输出
+ print(f"\n[模型工具] 加载模型失败!路径:{model_path}")
+ print(f"[模型工具] 异常类型:{type(e).__name__}")
+ print(f"[模型工具] 异常详情:{str(e)}")
+ print(f"[模型工具] 堆栈跟踪:\n{traceback.format_exc()}")
+ return None
diff --git a/ws/__pycache__/ws.cpython-310.pyc b/ws/__pycache__/ws.cpython-310.pyc
new file mode 100644
index 0000000..68d48df
Binary files /dev/null and b/ws/__pycache__/ws.cpython-310.pyc differ
diff --git a/ws/ws.py b/ws/ws.py
new file mode 100644
index 0000000..a64767d
--- /dev/null
+++ b/ws/ws.py
@@ -0,0 +1,444 @@
+import asyncio
+import datetime
+import json
+import os
+import base64
+from contextlib import asynccontextmanager
+from typing import Dict, Optional
+
+import cv2
+import numpy as np
+from Crypto.Cipher import AES
+from Crypto.Util.Padding import pad
+from fastapi import WebSocket, APIRouter, WebSocketDisconnect, FastAPI
+from core.detect import detectFrame
+from service.device_service import update_online_status_by_ip, get_unique_client_ips, get_is_need_handler_by_ip
+
+AES_SECRET_KEY = b"jr1vA6tfWMHOYi6UXw67UuO6fdak2rMa"
+AES_BLOCK_SIZE = 16 # AES固定块大小
+HEARTBEAT_INTERVAL = 10 # 心跳检查间隔(秒)
+HEARTBEAT_TIMEOUT = 30 # 客户端超时阈值(秒)
+WS_ENDPOINT = "/ws" # WebSocket端点路径
+FRAME_QUEUE_SIZE = 1 # 帧队列大小限制
+ONLINE_STATUS = 1
+OFFLINE_STATUS = 0
+
+def aes_encrypt(plaintext: str) -> dict:
+ try:
+ iv = os.urandom(AES_BLOCK_SIZE) # 随机IV(16字节)
+ cipher = AES.new(AES_SECRET_KEY, AES.MODE_CBC, iv)
+ padded_plaintext = pad(plaintext.encode("utf-8"), AES_BLOCK_SIZE)
+ ciphertext = base64.b64encode(cipher.encrypt(padded_plaintext)).decode("utf-8")
+ iv_base64 = base64.b64encode(iv).decode("utf-8")
+ return {
+ "ciphertext": ciphertext,
+ "iv": iv_base64,
+ "algorithm": "AES-CBC"
+ }
+ except Exception as e:
+ raise Exception(f"AES加密失败: {str(e)}") from e
+
+
+def get_current_time_str() -> str:
+ """获取格式化时间字符串"""
+ return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+
+# ------------------------------
+# 客户端连接类(保持不变)
+# ------------------------------
+class ClientConnection:
+ def __init__(self, websocket: WebSocket, client_ip: str):
+ self.websocket = websocket
+ self.client_ip = client_ip
+ self.last_heartbeat = datetime.datetime.now()
+ self.frame_queue = asyncio.Queue(maxsize=FRAME_QUEUE_SIZE)
+ self.consumer_task: Optional[asyncio.Task] = None
+ self.is_connected = False # 连接状态标记(用于连接管理,非业务状态)
+
+ def update_heartbeat(self):
+ """更新心跳时间"""
+ self.last_heartbeat = datetime.datetime.now()
+
+ def is_alive(self) -> bool:
+ """判断客户端是否存活(心跳未超时)"""
+ timeout_seconds = (datetime.datetime.now() - self.last_heartbeat).total_seconds()
+ return timeout_seconds < HEARTBEAT_TIMEOUT
+
+ def start_consumer(self):
+ """启动帧消费任务"""
+ self.consumer_task = asyncio.create_task(self.consume_frames())
+ return self.consumer_task
+
+ async def send_frame_permit(self):
+ """发送加密的帧许可信号(服务器→客户端)"""
+ try:
+ frame_permit_msg = {
+ "type": "frame",
+ "timestamp": get_current_time_str(),
+ "client_ip": self.client_ip
+ }
+ encrypted_msg = aes_encrypt(json.dumps(frame_permit_msg))
+ await self.websocket.send_json(encrypted_msg)
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 已发送加密帧许可")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 帧许可加密/发送失败 - {str(e)}")
+
+ async def consume_frames(self) -> None:
+ """消费队列中的明文图像帧并调用检测"""
+ try:
+ while self.is_connected: # 仅在有效连接状态下处理
+ frame_data = await self.frame_queue.get()
+ await self.send_frame_permit() # 回复帧许可
+ try:
+ await self.process_frame(frame_data)
+ finally:
+ self.frame_queue.task_done()
+ except asyncio.CancelledError:
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 帧消费任务已取消")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 帧消费错误 - {str(e)}")
+
+ async def process_frame(self, frame_data: bytes) -> None:
+ """处理图像帧(调用检测接口)"""
+ nparr = np.frombuffer(frame_data, np.uint8)
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
+ if img is None:
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 无法解析明文图像")
+ return
+
+ try:
+ # 仅保留检测调用,移除数据库记录相关逻辑
+ await asyncio.to_thread(detectFrame, self.client_ip, img)
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 图像检测调用完成")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{self.client_ip}: 检测调用失败 - {str(e)}")
+
+
+# ------------------------------
+# 连接管理与全局任务(基础结构不变,新增状态更新逻辑)
+# ------------------------------
+connected_clients: Dict[str, ClientConnection] = {}
+heartbeat_task: Optional[asyncio.Task] = None
+
+
+
+
+# ------------------------------
+# 心跳检查任务(超时后更新设备为下线)
+# ------------------------------
+async def heartbeat_checker():
+ """全局心跳检查(清理超时连接 + 标记设备为下线)"""
+ while True:
+ current_time = get_current_time_str()
+ # 筛选超时连接(心跳超时且处于连接状态)
+ timeout_ips = [
+ ip for ip, conn in connected_clients.items()
+ if conn.is_connected and not conn.is_alive()
+ ]
+
+ if timeout_ips:
+ print(f"[{current_time}] 心跳检查: {len(timeout_ips)}个客户端超时(IP: {timeout_ips})")
+ for ip in timeout_ips:
+ try:
+ conn = connected_clients[ip]
+ try:
+ update_success = update_online_status_by_ip(ip, OFFLINE_STATUS)
+ if update_success:
+ print(f"[{current_time}] 客户端{ip}: 心跳超时,设备状态更新为下线成功")
+ else:
+ print(f"[{current_time}] 客户端{ip}: 心跳超时,设备状态更新为下线失败")
+ except Exception as e:
+ print(f"[{current_time}] 客户端{ip}: 心跳超时更新下线状态异常 - {str(e)}")
+
+ # 2. 原有逻辑:标记连接无效并清理资源
+ conn.is_connected = False
+ if conn.consumer_task and not conn.consumer_task.done():
+ conn.consumer_task.cancel()
+ await conn.websocket.close(code=1008, reason="心跳超时(30秒无响应)")
+ print(f"[{current_time}] 客户端{ip}: 已关闭超时连接")
+ except Exception as e:
+ print(f"[{current_time}] 客户端{ip}: 超时连接清理失败 - {str(e)}")
+ finally:
+ # 从连接管理列表移除
+ if ip in connected_clients:
+ connected_clients.pop(ip, None)
+ else:
+ print(f"[{current_time}] 心跳检查: {len(connected_clients)}个客户端在线")
+
+ await asyncio.sleep(HEARTBEAT_INTERVAL)
+
+
+# ------------------------------
+# 心跳确认发送(保持不变)
+# ------------------------------
+async def send_heartbeat_ack(conn: ClientConnection):
+ """发送加密的心跳确认(服务器→客户端)"""
+ try:
+ heartbeat_ack_msg = {
+ "type": "heart",
+ "timestamp": get_current_time_str(),
+ "client_ip": conn.client_ip
+ }
+ encrypted_msg = aes_encrypt(json.dumps(heartbeat_ack_msg))
+ await conn.websocket.send_json(encrypted_msg)
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 已发送加密心跳确认")
+ return True
+ except Exception as e:
+ # 心跳确认失败时清理连接
+ if conn.client_ip in connected_clients:
+ connected_clients.pop(conn.client_ip, None)
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 心跳确认失败 - {str(e)}")
+ return False
+
+
+# ------------------------------
+# 文本消息处理(新增:心跳时更新设备为在线)
+# ------------------------------
+async def handle_text_msg(conn: ClientConnection, text: str):
+ """处理客户端明文文本消息(心跳+状态更新)"""
+ try:
+ msg = json.loads(text)
+ if msg.get("type") == "heart":
+ # 1. 原有逻辑:更新心跳时间
+ conn.update_heartbeat()
+ # 2. 新增逻辑:更新设备状态为在线
+ try:
+ update_success = update_online_status_by_ip(conn.client_ip, ONLINE_STATUS)
+ if update_success:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 设备在线状态更新为在线成功")
+ else:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 设备在线状态更新为在线失败")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 更新在线状态时异常 - {str(e)}")
+ # 3. 原有逻辑:发送心跳确认
+ await send_heartbeat_ack(conn)
+ else:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 未知明文文本类型({msg.get('type')})")
+ except json.JSONDecodeError:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 无效JSON格式(明文文本)")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 明文文本消息处理失败 - {str(e)}")
+
+
+# ------------------------------
+# 路由与生命周期(保持不变)
+# ------------------------------
+ws_router = APIRouter()
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ """应用生命周期:启动/停止心跳任务"""
+ global heartbeat_task
+ # 启动心跳检查任务
+ heartbeat_task = asyncio.create_task(heartbeat_checker())
+ print(f"[{get_current_time_str()}] 心跳检查任务启动(ID: {id(heartbeat_task)})")
+ yield
+ # 应用关闭时清理资源
+ if heartbeat_task and not heartbeat_task.done():
+ heartbeat_task.cancel()
+ await heartbeat_task
+ print(f"[{get_current_time_str()}] 心跳检查任务已取消")
+
+ # 清理所有活跃连接 + 标记为下线
+ for conn in connected_clients.values():
+ conn.is_connected = False
+ # 关闭前更新设备为下线
+ try:
+ update_online_status_by_ip(conn.client_ip, OFFLINE_STATUS)
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 服务关闭,设备状态更新为下线")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 服务关闭更新状态异常 - {str(e)}")
+ # 原有清理逻辑
+ if conn.consumer_task and not conn.consumer_task.done():
+ conn.consumer_task.cancel()
+ try:
+ await conn.websocket.close(code=1001, reason="服务关闭")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{conn.client_ip}: 服务关闭时连接清理失败 - {str(e)}")
+ connected_clients.clear()
+
+
+# ------------------------------
+# WebSocket端点(核心修改:连接校验 + 断开时状态更新)
+# ------------------------------
+@ws_router.websocket(WS_ENDPOINT)
+async def websocket_endpoint(websocket: WebSocket):
+ """WebSocket连接处理(新增:设备校验 + 断开下线)"""
+ client_ip = websocket.client.host if websocket.client else "unknown_ip"
+ current_time = get_current_time_str()
+ new_conn: Optional[ClientConnection] = None # 用于后续清理
+
+ try:
+ # ------------------------------
+ # 新增1:设备合法性校验(连接前拦截)
+ # ------------------------------
+ try:
+ valid_ips = get_unique_client_ips() # 获取合法设备列表
+ if_handler = get_is_need_handler_by_ip(client_ip)
+ except Exception as e:
+ print(f"[{current_time}] 客户端{client_ip}: 合法设备列表获取失败 - {str(e)}")
+ await websocket.close(code=1011, reason="服务器内部错误(合法设备校验失败)")
+ return
+
+ if client_ip not in valid_ips:
+ print(f"[{current_time}] 客户端{client_ip}: 不在合法设备列表中,拒绝连接")
+ await websocket.close(code=1008, reason="未授权设备(不在合法设备列表)")
+ return
+
+ if if_handler:
+ print(f"[{current_time}] 客户端{client_ip}: 设备需要处理,拒绝连接")
+ await websocket.close(code=1008, reason="未授权设备(需要处理)")
+ return
+
+ # 校验通过,接受连接
+ await websocket.accept()
+ print(f"[{current_time}] 客户端{client_ip}: 已接受连接请求(合法设备)")
+
+ # ------------------------------
+ # 原有逻辑:处理旧连接替换
+ # ------------------------------
+ if client_ip in connected_clients:
+ old_conn = connected_clients[client_ip]
+ old_conn.is_connected = False
+ # 旧连接下线更新(新增)
+ try:
+ update_online_status_by_ip(client_ip, OFFLINE_STATUS)
+ print(f"[{current_time}] 客户端{client_ip}: 旧连接替换,设备状态更新为下线")
+ except Exception as e:
+ print(f"[{current_time}] 客户端{client_ip}: 旧连接替换更新状态异常 - {str(e)}")
+ # 原有清理旧连接
+ if old_conn.consumer_task and not old_conn.consumer_task.done():
+ old_conn.consumer_task.cancel()
+ await old_conn.websocket.close(code=1008, reason="新连接建立(替换旧连接)")
+ connected_clients.pop(client_ip)
+ print(f"[{current_time}] 客户端{client_ip}: 已关闭旧连接(新连接替换)")
+
+ # ------------------------------
+ # 原有逻辑:初始化新连接
+ # ------------------------------
+ new_conn = ClientConnection(websocket, client_ip)
+ new_conn.is_connected = True
+ connected_clients[client_ip] = new_conn
+ new_conn.start_consumer()
+
+ # 发送初始帧许可
+ await new_conn.send_frame_permit()
+ # 新连接上线更新(新增)
+ try:
+ update_online_status_by_ip(client_ip, ONLINE_STATUS)
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 新连接初始化,设备状态更新为在线")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 新连接初始化更新状态异常 - {str(e)}")
+
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 连接初始化完成!当前在线数: {len(connected_clients)}")
+
+ # ------------------------------
+ # 原有逻辑:循环接收消息
+ # ------------------------------
+ while new_conn.is_connected:
+ data = await websocket.receive()
+ if "text" in data:
+ await handle_text_msg(new_conn, data["text"])
+ elif "bytes" in data:
+ frame_data = data["bytes"]
+ try:
+ new_conn.frame_queue.put_nowait(frame_data)
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 明文图像({len(frame_data)}字节)入队")
+ except asyncio.QueueFull:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 帧队列已满,丢弃当前图像数据")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 明文图像处理失败 - {str(e)}")
+
+ # ------------------------------
+ # 新增2:主动断开连接时更新下线
+ # ------------------------------
+ except WebSocketDisconnect as e:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 主动断开连接(代码: {e.code})")
+ # 主动断开时更新下线
+ try:
+ update_success = update_online_status_by_ip(client_ip, OFFLINE_STATUS)
+ if update_success:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 主动断开,设备状态更新为下线成功")
+ else:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 主动断开,设备状态更新为下线失败")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 主动断开更新下线状态异常 - {str(e)}")
+
+ # ------------------------------
+ # 原有异常处理
+ # ------------------------------
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 连接异常 - {str(e)[:50]}")
+ # 异常断开时更新下线(新增)
+ try:
+ update_online_status_by_ip(client_ip, OFFLINE_STATUS)
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 异常断开,设备状态更新为下线")
+ except Exception as ex:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 异常断开更新状态异常 - {str(ex)}")
+
+ # ------------------------------
+ # 新增3:最终清理时确保状态下线
+ # ------------------------------
+ finally:
+ # 连接结束时清理资源(仅当连接曾有效初始化)
+ if client_ip in connected_clients:
+ conn = connected_clients[client_ip]
+ conn.is_connected = False
+ # 再次确认下线(防止漏更)
+ try:
+ update_online_status_by_ip(client_ip, OFFLINE_STATUS)
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 连接清理,设备状态确认下线")
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 连接清理确认状态异常 - {str(e)}")
+ # 原有清理逻辑
+ if conn.consumer_task and not conn.consumer_task.done():
+ conn.consumer_task.cancel()
+ connected_clients.pop(client_ip, None)
+
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 连接资源已清理!当前在线数: {len(connected_clients)}")
+
+
+# ------------------------------
+# 原有功能:客户端连接状态判断(保持不变)
+# ------------------------------
+async def send_message_to_client(client_ip: str, json_data: str) -> bool:
+ """向指定IP的客户端发送加密消息(保留基础通信能力)"""
+ try:
+ if client_ip not in connected_clients:
+ print(f"[{get_current_time_str()}] 发送消息失败: 客户端{client_ip}不在线")
+ return False
+
+ client_conn = connected_clients[client_ip]
+ if not client_conn.is_connected or not client_conn.is_alive():
+ print(f"[{get_current_time_str()}] 发送消息失败: 客户端{client_ip}连接无效(已断开/超时)")
+ connected_clients.pop(client_ip, None)
+ return False
+
+ encrypted_msg = aes_encrypt(json_data)
+ await client_conn.websocket.send_json(encrypted_msg)
+ parsed_data = json.loads(json_data)
+ print(f"[{get_current_time_str()}] 已向客户端{client_ip}发送消息: {parsed_data.get('type')}")
+ return True
+ except Exception as e:
+ print(f"[{get_current_time_str()}] 向客户端{client_ip}发送消息失败: {str(e)}")
+ if client_ip in connected_clients:
+ connected_clients.pop(client_ip, None)
+ return False
+
+
+def is_client_connected(client_ip: str) -> bool:
+ """判断指定客户端IP是否处于有效连接状态"""
+ if client_ip not in connected_clients:
+ print(f"[{get_current_time_str()}] 客户端{client_ip}: 未在活跃连接列表中(连接状态:False)")
+ return False
+
+ client_conn = connected_clients[client_ip]
+ connection_valid = client_conn.is_connected and client_conn.is_alive()
+ print(
+ f"[{get_current_time_str()}] 客户端{client_ip}: "
+ f"连接标记={client_conn.is_connected}, 心跳存活={client_conn.is_alive()}, "
+ f"最终有效状态={connection_valid}"
+ )
+ return connection_valid
\ No newline at end of file