|
@@ -2,8 +2,9 @@ import json
|
|
|
import logging
|
|
import logging
|
|
|
import threading
|
|
import threading
|
|
|
import time
|
|
import time
|
|
|
|
|
+from datetime import datetime
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
-from typing import Optional
|
|
|
|
|
|
|
+from typing import Any, Optional
|
|
|
|
|
|
|
|
import paho.mqtt.client as mqtt
|
|
import paho.mqtt.client as mqtt
|
|
|
|
|
|
|
@@ -34,6 +35,7 @@ class MqttCaptureService:
|
|
|
def __init__(self) -> None:
|
|
def __init__(self) -> None:
|
|
|
self._mqtt = settings.mqtt
|
|
self._mqtt = settings.mqtt
|
|
|
self._static = settings.static
|
|
self._static = settings.static
|
|
|
|
|
+ self._cloud = settings.cloud
|
|
|
self._camera = get_camera_service()
|
|
self._camera = get_camera_service()
|
|
|
|
|
|
|
|
def capture_pair(self) -> dict[str, object]:
|
|
def capture_pair(self) -> dict[str, object]:
|
|
@@ -41,16 +43,18 @@ class MqttCaptureService:
|
|
|
raise MqttCaptureBusyError("当前已有一个 MQTT 拍照任务正在执行,请稍后再试。")
|
|
raise MqttCaptureBusyError("当前已有一个 MQTT 拍照任务正在执行,请稍后再试。")
|
|
|
|
|
|
|
|
self._static.ensure_directories()
|
|
self._static.ensure_directories()
|
|
|
- request_id = f"req-{int(time.time() * 1000)}"
|
|
|
|
|
front_path = self._static.front_image_path
|
|
front_path = self._static.front_image_path
|
|
|
back_path = self._static.back_image_path
|
|
back_path = self._static.back_image_path
|
|
|
- state = {
|
|
|
|
|
|
|
+ request_id = f"req-{int(time.time() * 1000)}"
|
|
|
|
|
+ state: dict[str, Any] = {
|
|
|
"connected": threading.Event(),
|
|
"connected": threading.Event(),
|
|
|
"done": threading.Event(),
|
|
"done": threading.Event(),
|
|
|
|
|
+ "recognized": threading.Event(),
|
|
|
"error": "",
|
|
"error": "",
|
|
|
"status": None,
|
|
"status": None,
|
|
|
"front_saved": False,
|
|
"front_saved": False,
|
|
|
"back_saved": False,
|
|
"back_saved": False,
|
|
|
|
|
+ "recognizedInfoDTO": None,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
client = mqtt.Client(
|
|
client = mqtt.Client(
|
|
@@ -136,6 +140,12 @@ class MqttCaptureService:
|
|
|
|
|
|
|
|
if image_id == "1":
|
|
if image_id == "1":
|
|
|
state["front_saved"] = True
|
|
state["front_saved"] = True
|
|
|
|
|
+ threading.Thread(
|
|
|
|
|
+ target=self._recognize_front_image,
|
|
|
|
|
+ args=(front_path, state),
|
|
|
|
|
+ daemon=True,
|
|
|
|
|
+ ).start()
|
|
|
|
|
+
|
|
|
if image_id == "2":
|
|
if image_id == "2":
|
|
|
state["back_saved"] = True
|
|
state["back_saved"] = True
|
|
|
|
|
|
|
@@ -185,16 +195,17 @@ class MqttCaptureService:
|
|
|
raise MqttCaptureError(f"流程未正常结束,最终 status={state['status']}")
|
|
raise MqttCaptureError(f"流程未正常结束,最终 status={state['status']}")
|
|
|
|
|
|
|
|
if not state["front_saved"] or not front_path.exists():
|
|
if not state["front_saved"] or not front_path.exists():
|
|
|
- raise MqttCaptureError("没有拿到 1.jpg")
|
|
|
|
|
|
|
+ raise MqttCaptureError("没有拿到正面图片")
|
|
|
|
|
|
|
|
if not state["back_saved"] or not back_path.exists():
|
|
if not state["back_saved"] or not back_path.exists():
|
|
|
- raise MqttCaptureError("没有拿到 2.jpg")
|
|
|
|
|
|
|
+ raise MqttCaptureError("没有拿到背面图片")
|
|
|
|
|
+
|
|
|
|
|
+ state["recognized"].wait(timeout=self._cloud.recognize_timeout_seconds)
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
- "request_id": request_id,
|
|
|
|
|
- "status": state["status"],
|
|
|
|
|
- "front_path": front_path,
|
|
|
|
|
- "back_path": back_path,
|
|
|
|
|
|
|
+ "card_front_url": self._upload_image_to_cloud(front_path, "front"),
|
|
|
|
|
+ "card_back_url": self._upload_image_to_cloud(back_path, "back"),
|
|
|
|
|
+ "recognizedInfoDTO": state["recognizedInfoDTO"],
|
|
|
}
|
|
}
|
|
|
finally:
|
|
finally:
|
|
|
try:
|
|
try:
|
|
@@ -230,6 +241,95 @@ class MqttCaptureService:
|
|
|
client.publish(self._mqtt.camera_response_topic, json.dumps(payload), qos=1)
|
|
client.publish(self._mqtt.camera_response_topic, json.dumps(payload), qos=1)
|
|
|
logger.info("已发送拍照回执: %s", payload)
|
|
logger.info("已发送拍照回执: %s", payload)
|
|
|
|
|
|
|
|
|
|
+ def _recognize_front_image(self, front_path: Path, state: dict[str, Any]) -> None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ state["recognizedInfoDTO"] = self._recognize_card_info(front_path)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ state["recognized"].set()
|
|
|
|
|
+
|
|
|
|
|
+ def _upload_image_to_cloud(self, local_file_path: Path, side: str) -> Optional[str]:
|
|
|
|
|
+ if not local_file_path.exists():
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ from minio import Minio
|
|
|
|
|
+ except ImportError:
|
|
|
|
|
+ logger.warning("未安装 minio 依赖,跳过上传。")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ minio_client = Minio(
|
|
|
|
|
+ self._cloud.minio_endpoint,
|
|
|
|
|
+ access_key=self._cloud.minio_access_key,
|
|
|
|
|
+ secret_key=self._cloud.minio_secret_key,
|
|
|
|
|
+ secure=self._cloud.minio_secure,
|
|
|
|
|
+ )
|
|
|
|
|
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")
|
|
|
|
|
+ object_name = f"raspi_{side}_{timestamp}.jpg"
|
|
|
|
|
+ cloud_path = f"{self._cloud.minio_base_prefix}/{object_name}"
|
|
|
|
|
+
|
|
|
|
|
+ minio_client.fput_object(
|
|
|
|
|
+ self._cloud.minio_bucket,
|
|
|
|
|
+ cloud_path,
|
|
|
|
|
+ str(local_file_path),
|
|
|
|
|
+ content_type="image/jpeg",
|
|
|
|
|
+ )
|
|
|
|
|
+ return f"{self._cloud.minio_public_base_url}/{object_name}"
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.warning("上传图片到 MinIO 失败: %s", local_file_path, exc_info=True)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _recognize_card_info(self, local_file_path: Path) -> Optional[dict[str, str]]:
|
|
|
|
|
+ if not local_file_path.exists():
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ import requests
|
|
|
|
|
+ except ImportError:
|
|
|
|
|
+ logger.warning("未安装 requests 依赖,跳过卡牌识别。")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ with local_file_path.open("rb") as image_file:
|
|
|
|
|
+ response = requests.post(
|
|
|
|
|
+ self._cloud.recognize_api_url,
|
|
|
|
|
+ files={
|
|
|
|
|
+ "imgFile": (
|
|
|
|
|
+ local_file_path.name,
|
|
|
|
|
+ image_file,
|
|
|
|
|
+ "image/jpeg",
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ timeout=self._cloud.recognize_timeout_seconds,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ logger.warning("卡牌识别接口返回异常状态码: %s", response.status_code)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ result_list = response.json()
|
|
|
|
|
+ if not result_list or not isinstance(result_list, list):
|
|
|
|
|
+ logger.warning("卡牌识别接口返回了空结果或非法格式。")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ detail = (result_list[0] or {}).get("detail") or {}
|
|
|
|
|
+ path = detail.get("path") or ""
|
|
|
|
|
+ if path:
|
|
|
|
|
+ path = f"{self._cloud.data_prefix}{path}"
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "year": detail.get("year", ""),
|
|
|
|
|
+ "series": detail.get("series", ""),
|
|
|
|
|
+ "cardSet": detail.get("cardset", ""),
|
|
|
|
|
+ "player": detail.get("player", ""),
|
|
|
|
|
+ "confidentialId": detail.get("no", ""),
|
|
|
|
|
+ "limitId": "",
|
|
|
|
|
+ "path": path,
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.warning("卡牌识别请求失败: %s", local_file_path, exc_info=True)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def get_mqtt_capture_service() -> MqttCaptureService:
|
|
def get_mqtt_capture_service() -> MqttCaptureService:
|
|
|
return MqttCaptureService()
|
|
return MqttCaptureService()
|