完善牧安云哨-后端
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
|
||||||
|
from config.llm import llmVision
|
||||||
|
|
||||||
|
|
||||||
|
# -------- 定义状态 --------
|
||||||
|
class State(TypedDict):
|
||||||
|
image_url: str # 图像
|
||||||
|
content: str # 最终内容
|
||||||
|
|
||||||
|
|
||||||
|
def send_analyze(state: State, prompt_text: str):
|
||||||
|
messages = [
|
||||||
|
HumanMessage(
|
||||||
|
content=[
|
||||||
|
{"type": "text", "text": prompt_text},
|
||||||
|
{"type": "image_url", "image_url": {"url": state["image_url"]}},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return llmVision.invoke(messages).content
|
||||||
|
|
||||||
|
|
||||||
|
def analysis(state: State):
|
||||||
|
state["content"] = send_analyze(
|
||||||
|
state,
|
||||||
|
"""
|
||||||
|
提示词示例
|
||||||
|
你是一个图像分析助手。现在给你一张车的侧身照片,请你从图中分析车上运输的牲畜种类。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 牲畜种类可能是:牛、羊、猪、鸡、鸭、鹅。
|
||||||
|
2. 如果图中无法判断牲畜类型,请在备注字段 remark 中写明“无法识别”或你观察到的情况。
|
||||||
|
3. 不允许输出多余文字,直接返回 JSON。
|
||||||
|
|
||||||
|
JSON 示例格式:
|
||||||
|
{
|
||||||
|
"livestock_type": "<牲畜种类>", // 如果能识别就填牛/羊/猪/鸡/鸭/鹅
|
||||||
|
"remark": "<备注>" // 如果无法识别,写明原因;否则可留空
|
||||||
|
}
|
||||||
|
请确保输出的 JSON 可以被严格解析。
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------ 构建有向图 --------
|
||||||
|
workflow = StateGraph(State)
|
||||||
|
# 必须先从 START 指向 analysis
|
||||||
|
workflow.add_node("analysis", analysis)
|
||||||
|
workflow.set_entry_point("analysis")
|
||||||
|
workflow.add_edge("analysis", END)
|
||||||
|
graph = workflow.compile()
|
||||||
|
|
||||||
|
|
||||||
|
# 执行函数
|
||||||
|
|
||||||
|
|
||||||
|
async def get_vehicle_response(image_url: str):
|
||||||
|
final_state = graph.invoke(
|
||||||
|
{
|
||||||
|
"image_url": image_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 去掉 ```json 和 ``` 包裹
|
||||||
|
content_str = re.sub(r"^```json\s*|\s*```$", "", final_state["content"].strip())
|
||||||
|
# 把 JSON 字符串转为字典
|
||||||
|
try:
|
||||||
|
content_dict = json.loads(content_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("JSON解析失败")
|
||||||
|
content_dict = {}
|
||||||
|
|
||||||
|
return content_dict
|
||||||
+7
-4
@@ -4,6 +4,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from uvicorn import Config, Server
|
from uvicorn import Config, Server
|
||||||
|
|
||||||
|
from config.emqx import mqtt_client_async
|
||||||
from config.yolo import YOLOSingleton
|
from config.yolo import YOLOSingleton
|
||||||
from routers.Bot import botRouter
|
from routers.Bot import botRouter
|
||||||
from routers.Chat import chatRouter
|
from routers.Chat import chatRouter
|
||||||
@@ -18,6 +19,7 @@ from routers.Service import serviceRouter
|
|||||||
from routers.System import systemRouter
|
from routers.System import systemRouter
|
||||||
from routers.Vision import visionRouter
|
from routers.Vision import visionRouter
|
||||||
from routers.WS import iot_ws_router
|
from routers.WS import iot_ws_router
|
||||||
|
from service.RabbitMQ import sentinel_pull_analysis_async
|
||||||
|
|
||||||
|
|
||||||
async def ai_lab():
|
async def ai_lab():
|
||||||
@@ -63,6 +65,11 @@ async def main():
|
|||||||
YOLOSingleton.init_model()
|
YOLOSingleton.init_model()
|
||||||
# 主干AI实验室FastAPI服务
|
# 主干AI实验室FastAPI服务
|
||||||
task_api = asyncio.create_task(ai_lab())
|
task_api = asyncio.create_task(ai_lab())
|
||||||
|
# RabbitMQ服务
|
||||||
|
task_mq = asyncio.create_task(sentinel_pull_analysis_async())
|
||||||
|
# 等 HTTP 服务启动后再启动 MQTT
|
||||||
|
task_mqtt = asyncio.create_task(mqtt_client_async())
|
||||||
|
await asyncio.gather(task_api, task_mq, task_mqtt)
|
||||||
|
|
||||||
# MCP服务-ailab
|
# MCP服务-ailab
|
||||||
# endpoint_url_ai_lab = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
|
# endpoint_url_ai_lab = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
|
||||||
@@ -73,11 +80,7 @@ async def main():
|
|||||||
# endpoint_url_ql = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=8ZmCzp7FzsbxwHOg2%2FvBQkxrC3QWJiI%2B4iTfouExinjcT8ZgLwQfFUtgcMInI7St"
|
# endpoint_url_ql = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=8ZmCzp7FzsbxwHOg2%2FvBQkxrC3QWJiI%2B4iTfouExinjcT8ZgLwQfFUtgcMInI7St"
|
||||||
# task_mcp2 = asyncio.create_task(init_mcp_server(endpoint_url_ql))
|
# task_mcp2 = asyncio.create_task(init_mcp_server(endpoint_url_ql))
|
||||||
|
|
||||||
# RabbitMQ服务
|
|
||||||
# task_mq = asyncio.create_task(mq_pull_analysis_async())
|
|
||||||
|
|
||||||
# await asyncio.gather(task_api, task_mcp1, task_mcp2, task_mq)
|
# await asyncio.gather(task_api, task_mcp1, task_mcp2, task_mq)
|
||||||
await asyncio.gather(task_api)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from aiomqtt import Client
|
||||||
|
|
||||||
|
from config.redis import redis_client
|
||||||
|
from models.MqttTopic import MqttTopic
|
||||||
|
|
||||||
|
# ================= 配置区域 =================
|
||||||
|
MQTT_BROKER = "ai.ronsunny.cn"
|
||||||
|
MQTT_PORT = 8093
|
||||||
|
MQTT_PASSWORD = "123456"
|
||||||
|
TLS_CONTEXT = ssl.create_default_context()
|
||||||
|
|
||||||
|
# 默认连接后要订阅的 topic 配置
|
||||||
|
DEFAULT_SUBSCRIPTIONS = [
|
||||||
|
MqttTopic.from_parts(
|
||||||
|
project=None,
|
||||||
|
domain="status",
|
||||||
|
device_type="edge",
|
||||||
|
device_id=None,
|
||||||
|
resource="info",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
DEVICE_ID = None
|
||||||
|
MQTT_CLIENT: Client | None = None # 全局客户端
|
||||||
|
|
||||||
|
# Windows 平台下切换到 SelectorEventLoop
|
||||||
|
if sys.platform.lower() == "win32" or os.name.lower() == "nt":
|
||||||
|
from asyncio import set_event_loop_policy, WindowsSelectorEventLoopPolicy
|
||||||
|
|
||||||
|
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_id_simple():
|
||||||
|
try:
|
||||||
|
with open("/etc/machine-id") as f:
|
||||||
|
mid = f.read().strip()
|
||||||
|
if mid:
|
||||||
|
return mid
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
mac = uuid.getnode()
|
||||||
|
mac_str = ":".join(f"{(mac >> ele) & 0xff:02x}" for ele in range(40, -1, -8))
|
||||||
|
return f"{hostname}|{mac_str}"
|
||||||
|
|
||||||
|
|
||||||
|
# todo 这里需要订阅状态信息 设备发送信息 这里回复 vue前端发送指令 后端发送指令 设备接收指令
|
||||||
|
# ------------------ MQTT 封装 ------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def mqtt_publish(
|
||||||
|
project: str,
|
||||||
|
domain: str,
|
||||||
|
device_type: str,
|
||||||
|
device_id: str,
|
||||||
|
resource: str,
|
||||||
|
payload: str,
|
||||||
|
qos: int = 1,
|
||||||
|
):
|
||||||
|
"""发布消息(使用全局客户端)"""
|
||||||
|
if not MQTT_CLIENT:
|
||||||
|
raise RuntimeError("MQTT client is not initialized")
|
||||||
|
topic = f"{project}/{domain}/{device_type}/{device_id}/{resource}"
|
||||||
|
await MQTT_CLIENT.publish(topic, payload, qos=qos)
|
||||||
|
print(f"Published to {topic}: {payload}")
|
||||||
|
|
||||||
|
|
||||||
|
async def mqtt_publish_multiple(
|
||||||
|
targets: list[dict], resource: str, payload: str, qos: int = 1
|
||||||
|
):
|
||||||
|
"""群发消息"""
|
||||||
|
for target in targets:
|
||||||
|
await mqtt_publish(
|
||||||
|
domain=target["domain"],
|
||||||
|
device_type=target["device_type"],
|
||||||
|
device_id=target["device_id"],
|
||||||
|
resource=resource,
|
||||||
|
payload=payload,
|
||||||
|
qos=qos,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _mqtt_handle_messages():
|
||||||
|
"""后台循环处理消息"""
|
||||||
|
if not MQTT_CLIENT:
|
||||||
|
return
|
||||||
|
async for message in MQTT_CLIENT.messages:
|
||||||
|
topic = MqttTopic(message.topic)
|
||||||
|
print("收到消息:" + str(topic))
|
||||||
|
|
||||||
|
# 处理基础状态信息
|
||||||
|
if topic.domain == "status" and topic.resource == "info":
|
||||||
|
payload = json.loads(message.payload.decode())
|
||||||
|
redis_client.set_device_info(topic.device_id, payload)
|
||||||
|
|
||||||
|
|
||||||
|
async def mqtt_client_async():
|
||||||
|
global DEVICE_ID, MQTT_CLIENT
|
||||||
|
DEVICE_ID = get_device_id_simple()
|
||||||
|
print("服务端EMQX账号:", DEVICE_ID)
|
||||||
|
async with Client(
|
||||||
|
MQTT_BROKER,
|
||||||
|
port=MQTT_PORT,
|
||||||
|
username=DEVICE_ID,
|
||||||
|
password=MQTT_PASSWORD,
|
||||||
|
tls_context=TLS_CONTEXT,
|
||||||
|
identifier=DEVICE_ID,
|
||||||
|
) as client:
|
||||||
|
MQTT_CLIENT = client # 保存全局客户端
|
||||||
|
print("MQTT client connected")
|
||||||
|
|
||||||
|
# 订阅默认 topic
|
||||||
|
for topic in DEFAULT_SUBSCRIPTIONS:
|
||||||
|
await MQTT_CLIENT.subscribe(topic.to_topic())
|
||||||
|
print(f"Subscribed to default topic: {topic.to_topic()}")
|
||||||
|
|
||||||
|
# 启动消息处理循环
|
||||||
|
await _mqtt_handle_messages()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------ 示例主程序 ------------------
|
||||||
|
|
||||||
|
|
||||||
|
# async def main():
|
||||||
|
# await mqtt_client_async()
|
||||||
|
#
|
||||||
|
# # 示例:发布消息
|
||||||
|
# await mqtt_publish("status", "edge", DEVICE_ID, "heartbeat", '{"alive":true}')
|
||||||
|
#
|
||||||
|
# # 示例:群发
|
||||||
|
# targets = [
|
||||||
|
# {"domain": "cmd", "device_type": "edge", "device_id": "edge01"},
|
||||||
|
# {"domain": "cmd", "device_type": "edge", "device_id": "edge02"},
|
||||||
|
# ]
|
||||||
|
# await mqtt_publish_multiple(targets, "restart", '{"action":"restart"}')
|
||||||
@@ -22,7 +22,7 @@ def push_file(bucket_name, object_name, file_bytes, contents, content_type):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_upload_token(user_id, bucket_name, object_name, xpires=timedelta(minutes=15)):
|
def get_upload_token(bucket_name, object_name, xpires=timedelta(minutes=15)):
|
||||||
return minio_client.presigned_put_object(
|
return minio_client.presigned_put_object(
|
||||||
bucket_name=bucket_name, object_name=object_name, expires=xpires
|
bucket_name=bucket_name, object_name=object_name, expires=xpires
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from utils.GlobalVariable import LOCAL_IP
|
from utils.GlobalVariable import LOCAL_IP
|
||||||
|
|
||||||
RABBIT_HOST = LOCAL_IP
|
RABBIT_HOST = LOCAL_IP
|
||||||
RABBIT_VHOST = "bbit_ai"
|
|
||||||
RABBIT_USER = "ai_lab"
|
RABBIT_USER = "ai_lab"
|
||||||
RABBIT_PASSWORD = "123456"
|
RABBIT_PASSWORD = "123456"
|
||||||
QUEUE_NAME = "analysis_queue"
|
QUEUE_NAME = "analysis_queue"
|
||||||
|
RABBIT_VHOST = "bbit_ai"
|
||||||
|
|
||||||
|
SENTINEL_VHOST = "sentinel"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import redis
|
import redis
|
||||||
|
|
||||||
|
|
||||||
class RedisClient:
|
# ---------------- Redis Client ----------------
|
||||||
|
|
||||||
|
|
||||||
|
class RedisClient:
|
||||||
def __init__(self, config_path="config.yaml"):
|
def __init__(self, config_path="config.yaml"):
|
||||||
self.redis = redis.Redis(
|
self.redis = redis.Redis(
|
||||||
"10.10.12.101",
|
"10.10.12.101",
|
||||||
@@ -22,3 +24,24 @@ class RedisClient:
|
|||||||
def is_device_online(self, device_id: str) -> bool:
|
def is_device_online(self, device_id: str) -> bool:
|
||||||
key = f"device:online:{device_id}"
|
key = f"device:online:{device_id}"
|
||||||
return self.redis.exists(key) == 1
|
return self.redis.exists(key) == 1
|
||||||
|
|
||||||
|
def set_device_info(self, device_id: str, info: dict):
|
||||||
|
"""
|
||||||
|
存储完整设备信息到 redis hash
|
||||||
|
将 bool 转为 int
|
||||||
|
"""
|
||||||
|
key = f"device:info:{device_id}"
|
||||||
|
|
||||||
|
# 转换 bool 为 int
|
||||||
|
sanitized_info = {
|
||||||
|
k: (int(v) if isinstance(v, bool) else v) for k, v in info.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.redis.hmset(key, sanitized_info)
|
||||||
|
|
||||||
|
def get_device_info(self, device_id: str) -> dict:
|
||||||
|
key = f"device:info:{device_id}"
|
||||||
|
return self.redis.hgetall(key)
|
||||||
|
|
||||||
|
|
||||||
|
redis_client = RedisClient()
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import jwt
|
|
||||||
from jwt import PyJWTError
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from fastapi import Header, HTTPException, Depends
|
|
||||||
|
|
||||||
JWT_SECRET = "secret_jwt"
|
import jwt
|
||||||
|
from fastapi import Header, HTTPException
|
||||||
|
from jwt import PyJWTError
|
||||||
|
from starlette.websockets import WebSocketDisconnect
|
||||||
|
|
||||||
|
JWT_SECRET = "secret_jwt"
|
||||||
JWT_ALGORITHM = "HS256"
|
JWT_ALGORITHM = "HS256"
|
||||||
JWT_AUDIENCE = "snowflake-ink"
|
JWT_AUDIENCE = "snowflake-ink"
|
||||||
JWT_ISSUER = "https://snowflake.ink/"
|
JWT_ISSUER = "https://snowflake.ink/"
|
||||||
|
|
||||||
|
|
||||||
def get_user_id_from_token(token: str = Header(..., alias="Authorization")) -> UUID:
|
def get_user_id_from_token(token: str = Header(..., alias="Authorization")) -> UUID:
|
||||||
"""
|
"""
|
||||||
从 Authorization 头解析 token,并返回 user_id
|
从 Authorization 头解析 token,并返回 user_id
|
||||||
@@ -24,7 +27,7 @@ def get_user_id_from_token(token: str = Header(..., alias="Authorization")) -> U
|
|||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
algorithms=[JWT_ALGORITHM],
|
algorithms=[JWT_ALGORITHM],
|
||||||
audience=JWT_AUDIENCE,
|
audience=JWT_AUDIENCE,
|
||||||
issuer=JWT_ISSUER
|
issuer=JWT_ISSUER,
|
||||||
)
|
)
|
||||||
except PyJWTError:
|
except PyJWTError:
|
||||||
raise HTTPException(status_code=401, detail="Token is missing or invalid")
|
raise HTTPException(status_code=401, detail="Token is missing or invalid")
|
||||||
@@ -36,4 +39,28 @@ def get_user_id_from_token(token: str = Header(..., alias="Authorization")) -> U
|
|||||||
if not user_id:
|
if not user_id:
|
||||||
raise HTTPException(status_code=401, detail="User ID not found in token")
|
raise HTTPException(status_code=401, detail="User ID not found in token")
|
||||||
|
|
||||||
return UUID(user_id)
|
return UUID(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_id_from_token_from_ws(token: str) -> UUID:
|
||||||
|
if token.startswith("Bearer "):
|
||||||
|
token = token[7:]
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token,
|
||||||
|
JWT_SECRET,
|
||||||
|
algorithms=[JWT_ALGORITHM],
|
||||||
|
audience=JWT_AUDIENCE,
|
||||||
|
issuer=JWT_ISSUER,
|
||||||
|
)
|
||||||
|
except PyJWTError:
|
||||||
|
raise WebSocketDisconnect() # token 无效就断开
|
||||||
|
|
||||||
|
if payload.get("token_type") != "access_token":
|
||||||
|
raise WebSocketDisconnect()
|
||||||
|
|
||||||
|
user_id = payload.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise WebSocketDisconnect()
|
||||||
|
|
||||||
|
return UUID(user_id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
|
|
||||||
|
from config.minIO import get_temp_url
|
||||||
from config.pgDb import pg_pool
|
from config.pgDb import pg_pool
|
||||||
from utils.MyUtils import format_datetime, is_valid_uuid
|
from utils.MyUtils import format_datetime, is_valid_uuid
|
||||||
|
|
||||||
@@ -201,3 +202,213 @@ def delete_device_db(id: str) -> int:
|
|||||||
cursor.execute("DELETE FROM iot_users WHERE id=%s;", (id,))
|
cursor.execute("DELETE FROM iot_users WHERE id=%s;", (id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount
|
return cursor.rowcount
|
||||||
|
|
||||||
|
|
||||||
|
def delete_update_db(id: str) -> int:
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM iot_update WHERE id = %s;",
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
|
||||||
|
def get_update_list_db_page(
|
||||||
|
page: int,
|
||||||
|
page_size: int,
|
||||||
|
id=None,
|
||||||
|
code=None,
|
||||||
|
dept_id=None,
|
||||||
|
startTime=None,
|
||||||
|
endTime=None,
|
||||||
|
):
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if id is not None:
|
||||||
|
conditions.append("u.id::text LIKE %s")
|
||||||
|
params.append(f"%{id}%")
|
||||||
|
|
||||||
|
# ---- 版本 / 升级代码 ----
|
||||||
|
if code is not None:
|
||||||
|
conditions.append("u.code = %s")
|
||||||
|
params.append(code)
|
||||||
|
|
||||||
|
# ---- 部门 ----
|
||||||
|
if dept_id and is_valid_uuid(dept_id):
|
||||||
|
conditions.append("u.dept_id = %s")
|
||||||
|
params.append(dept_id)
|
||||||
|
|
||||||
|
# ---- 时间过滤 ----
|
||||||
|
if startTime:
|
||||||
|
conditions.append("u.created_at >= %s")
|
||||||
|
params.append(startTime)
|
||||||
|
|
||||||
|
if endTime:
|
||||||
|
conditions.append("u.created_at <= %s")
|
||||||
|
params.append(endTime)
|
||||||
|
|
||||||
|
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
|
||||||
|
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
|
||||||
|
# ---- 总数 ----
|
||||||
|
count_sql = f"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM iot_update u
|
||||||
|
{where_clause};
|
||||||
|
"""
|
||||||
|
cursor.execute(count_sql, params)
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# ---- 列表 ----
|
||||||
|
list_sql = f"""
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.code,
|
||||||
|
u.dept_id,
|
||||||
|
sd.name AS dept_name,
|
||||||
|
u.remark,
|
||||||
|
u.oss,
|
||||||
|
u.size,
|
||||||
|
u.created_at
|
||||||
|
FROM iot_update u
|
||||||
|
LEFT JOIN sys_dept sd ON u.dept_id = sd.id
|
||||||
|
{where_clause}
|
||||||
|
ORDER BY u.created_at DESC
|
||||||
|
LIMIT %s OFFSET %s;
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(list_sql, params + [page_size, offset])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for r in rows:
|
||||||
|
(
|
||||||
|
update_id,
|
||||||
|
code,
|
||||||
|
dept_id,
|
||||||
|
dept_name,
|
||||||
|
remark,
|
||||||
|
oss,
|
||||||
|
size,
|
||||||
|
created_at,
|
||||||
|
) = r
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"id": update_id,
|
||||||
|
"code": code,
|
||||||
|
"dept_id": dept_id,
|
||||||
|
"dept_name": dept_name,
|
||||||
|
"remark": remark,
|
||||||
|
"oss_url": oss,
|
||||||
|
"size": size,
|
||||||
|
"created_at": format_datetime(created_at),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return result, total
|
||||||
|
|
||||||
|
|
||||||
|
def insert_update(data: dict) -> str:
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO iot_update
|
||||||
|
(code, dept_id, remark, oss, size)
|
||||||
|
VALUES
|
||||||
|
(%s, %s, %s, %s, %s)
|
||||||
|
RETURNING id;
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
data.get("code"),
|
||||||
|
data.get("dept_id"),
|
||||||
|
data.get("remark"),
|
||||||
|
data.get("uploadId"),
|
||||||
|
data.get("size"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
update_id = cursor.fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
return update_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_update_package(device_id: str | None = None):
|
||||||
|
"""
|
||||||
|
根据设备 ID 获取所属组织最新版本的更新包信息
|
||||||
|
返回示例:
|
||||||
|
{
|
||||||
|
"version": 1001,
|
||||||
|
"url": "https://xxx",
|
||||||
|
"notes": "更新内容描述"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not device_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
sql_get_dept = """
|
||||||
|
SELECT dept_id
|
||||||
|
FROM iot_users
|
||||||
|
WHERE name = %s
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
sql_get_package = """
|
||||||
|
SELECT code, oss, remark
|
||||||
|
FROM iot_update
|
||||||
|
WHERE dept_id = %s
|
||||||
|
ORDER BY code DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
# 1. 查询设备所属组织
|
||||||
|
cursor.execute(sql_get_dept, (device_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dept_id = row[0]
|
||||||
|
|
||||||
|
# 2. 查询该组织最新更新包
|
||||||
|
cursor.execute(sql_get_package, (dept_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
version, oss_path, content = row
|
||||||
|
return {
|
||||||
|
"version": version,
|
||||||
|
"url": get_temp_url("iot-update", oss_path),
|
||||||
|
"notes": content,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def getMaxCodeByDeptId(dept_id: str | None = None) -> int:
|
||||||
|
"""
|
||||||
|
根据组织ID获取 iot_update_package 最大 code,并在结果上加 1
|
||||||
|
返回整数,如果没有记录则返回 1
|
||||||
|
"""
|
||||||
|
if not dept_id:
|
||||||
|
return 0 # dept_id 为空直接返回初始版本号 1
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
SELECT MAX(code)
|
||||||
|
FROM iot_update
|
||||||
|
WHERE dept_id = %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(sql, (dept_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
max_code = row[0] if row and row[0] is not None else 0
|
||||||
|
return max_code
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from config.minIO import get_temp_url_dict
|
from config.minIO import get_temp_url_dict
|
||||||
from config.pgDb import pg_pool
|
from config.pgDb import pg_pool
|
||||||
|
from models.SentinelRecordRequest import SentinelRecordRequest
|
||||||
from utils.MyUtils import format_datetime
|
from utils.MyUtils import format_datetime
|
||||||
|
|
||||||
|
|
||||||
@@ -259,3 +260,55 @@ def delete_sentinel_record_db(id: str) -> int:
|
|||||||
cursor.execute("DELETE FROM sentinel_records WHERE id=%s;", (id,))
|
cursor.execute("DELETE FROM sentinel_records WHERE id=%s;", (id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount
|
return cursor.rowcount
|
||||||
|
|
||||||
|
|
||||||
|
def saveSentinelRecord(data: SentinelRecordRequest) -> str:
|
||||||
|
sql = """
|
||||||
|
INSERT INTO sentinel_records (
|
||||||
|
license_plate,
|
||||||
|
license_plate_image,
|
||||||
|
vehicle_type,
|
||||||
|
vehicle_image
|
||||||
|
)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
RETURNING id;
|
||||||
|
"""
|
||||||
|
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
sql,
|
||||||
|
(
|
||||||
|
data.LicensePlate,
|
||||||
|
data.LicensePlateImage,
|
||||||
|
data.VehicleType,
|
||||||
|
data.VehicleImage,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
new_id = cursor.fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
return str(new_id)
|
||||||
|
|
||||||
|
|
||||||
|
def update_sentinel_record(
|
||||||
|
id: str, livestock_type: str, remark: str, dept_id: str
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
根据 id 更新 sentinel_records 表中的 livestock_type 和 dept_id
|
||||||
|
"""
|
||||||
|
sql = """
|
||||||
|
UPDATE sentinel_records
|
||||||
|
SET livestock_type = %s,
|
||||||
|
remark = %s,
|
||||||
|
dept_id = %s,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = %s
|
||||||
|
RETURNING id;
|
||||||
|
"""
|
||||||
|
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(sql, (livestock_type, remark, dept_id, id))
|
||||||
|
record = cursor.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return record is not None
|
||||||
|
|||||||
@@ -942,11 +942,55 @@ def get_dept_ids_by_user_id(user_id: UUID) -> list:
|
|||||||
return dept_ids
|
return dept_ids
|
||||||
|
|
||||||
|
|
||||||
def get_dept_id_by_user_id(user_id: UUID) -> list:
|
def get_dept_id_by_user_id(user_id: str) -> str:
|
||||||
# 第一步:通过 user_id 查找其所属的 dept_id
|
# 通过 user_id 查找其所属的 dept_id
|
||||||
with pg_pool.getConn() as conn:
|
with pg_pool.getConn() as conn:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute("SELECT dept_id FROM users WHERE id = %s", (user_id,))
|
cursor.execute("SELECT dept_id FROM users WHERE id = %s", (user_id,))
|
||||||
dept_id = cursor.fetchone()
|
dept_id = cursor.fetchone()
|
||||||
dept_id = dept_id[0]
|
dept_id = dept_id[0]
|
||||||
|
return str(dept_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dept_id_by_iot_user_name(user_id: UUID) -> str:
|
||||||
|
# 通过 iot_user_id 查找其所属的 dept_id
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT dept_id FROM iot_users WHERE name = %s", (user_id,))
|
||||||
|
dept_id = cursor.fetchone()
|
||||||
|
dept_id = dept_id[0]
|
||||||
return dept_id
|
return dept_id
|
||||||
|
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def get_dept_ids_by_dept_id(dept_id: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
获取当前部门 ID 以及其所有父部门 ID(递归向上)
|
||||||
|
返回顺序:从当前部门一直到最顶层父部门
|
||||||
|
"""
|
||||||
|
with pg_pool.getConn() as conn:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
WITH RECURSIVE dept_tree AS (
|
||||||
|
-- 起点:当前部门
|
||||||
|
SELECT id, parent_id
|
||||||
|
FROM sys_dept
|
||||||
|
WHERE id = %s
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- 向上递归找父部门
|
||||||
|
SELECT d.id, d.parent_id
|
||||||
|
FROM sys_dept d
|
||||||
|
INNER JOIN dept_tree dt ON d.id = dt.parent_id
|
||||||
|
)
|
||||||
|
SELECT id FROM dept_tree;
|
||||||
|
""",
|
||||||
|
(dept_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [str(row[0]) for row in rows]
|
||||||
|
|||||||
@@ -1,25 +1,50 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import WebSocket
|
from fastapi import WebSocket
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.active_connections: List[WebSocket] = []
|
self.active_connections: List[dict] = [] # 保存 websocket 和用户信息
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket):
|
# proj_id:0:在线状态 1:畜牧车辆进入
|
||||||
|
async def connect(
|
||||||
|
self, websocket: WebSocket, user_id: UUID, dept_id: str, proj_id: int
|
||||||
|
):
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
self.active_connections.append(websocket)
|
self.active_connections.append(
|
||||||
|
{
|
||||||
|
"ws": websocket,
|
||||||
|
"user_id": user_id,
|
||||||
|
"dept_id": dept_id,
|
||||||
|
"proj_id": proj_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def disconnect(self, websocket: WebSocket):
|
async def disconnect(self, websocket: WebSocket):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
if websocket in self.active_connections:
|
self.active_connections = [
|
||||||
self.active_connections.remove(websocket)
|
conn for conn in self.active_connections if conn["ws"] != websocket
|
||||||
|
]
|
||||||
|
|
||||||
async def broadcast(self, message: dict):
|
async def noticeOnlineStatus(self, message: dict):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
for ws in self.active_connections:
|
for conn in self.active_connections:
|
||||||
await ws.send_json(message)
|
if conn["proj_id"] == 0:
|
||||||
|
await conn["ws"].send_json(message)
|
||||||
|
|
||||||
|
async def noticeSentinel(
|
||||||
|
self, message: dict, target_departments: List[UUID] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
target_departments: 指定哪些部门能收到消息
|
||||||
|
"""
|
||||||
|
async with self.lock:
|
||||||
|
for conn in self.active_connections:
|
||||||
|
if target_departments:
|
||||||
|
if conn["proj_id"] == 1 and conn["dept_id"] in target_departments:
|
||||||
|
await conn["ws"].send_json(message)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class IotDeviceCommandRequest(BaseModel):
|
||||||
|
id: str | None = None
|
||||||
|
command: str | None = None
|
||||||
|
project: str | None = None
|
||||||
|
device_type: str | None = None
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class MqttTopic:
|
||||||
|
"""
|
||||||
|
封装 MQTT topic,根据规则:
|
||||||
|
project/domain/deviceType/deviceId/resource
|
||||||
|
"""
|
||||||
|
|
||||||
|
LEVELS = 5
|
||||||
|
|
||||||
|
def __init__(self, topic: str):
|
||||||
|
self.raw = str(topic)
|
||||||
|
parts = self.raw.split("/")
|
||||||
|
|
||||||
|
# 不足的层级用 None 补齐,避免属性缺失
|
||||||
|
parts += [None] * (self.LEVELS - len(parts))
|
||||||
|
|
||||||
|
self.project: Optional[str] = parts[0]
|
||||||
|
self.domain: Optional[str] = parts[1]
|
||||||
|
self.device_type: Optional[str] = parts[2]
|
||||||
|
self.device_id: Optional[str] = parts[3]
|
||||||
|
self.resource: Optional[str] = parts[4]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_parts(
|
||||||
|
cls,
|
||||||
|
project: Optional[str] = None,
|
||||||
|
domain: Optional[str] = None,
|
||||||
|
device_type: Optional[str] = None,
|
||||||
|
device_id: Optional[str] = None,
|
||||||
|
resource: Optional[str] = None,
|
||||||
|
) -> "MqttTopic":
|
||||||
|
"""
|
||||||
|
通过结构化参数构造 topic
|
||||||
|
None -> '+'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _v(v: Optional[str]) -> str:
|
||||||
|
return "+" if v is None else str(v)
|
||||||
|
|
||||||
|
topic = "/".join(
|
||||||
|
map(
|
||||||
|
_v,
|
||||||
|
[
|
||||||
|
project,
|
||||||
|
domain,
|
||||||
|
device_type,
|
||||||
|
device_id,
|
||||||
|
resource,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cls(topic)
|
||||||
|
|
||||||
|
def to_topic(self) -> str:
|
||||||
|
"""
|
||||||
|
根据当前字段生成 topic(允许 '+')
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _v(v: Optional[str]) -> str:
|
||||||
|
return "+" if v is None else v
|
||||||
|
|
||||||
|
return "/".join(
|
||||||
|
map(
|
||||||
|
_v,
|
||||||
|
[
|
||||||
|
self.project,
|
||||||
|
self.domain,
|
||||||
|
self.device_type,
|
||||||
|
self.device_id,
|
||||||
|
self.resource,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(self) -> str:
|
||||||
|
"""
|
||||||
|
生成严格 topic(不允许 None / '+')
|
||||||
|
用于 publish 场景
|
||||||
|
"""
|
||||||
|
parts = [
|
||||||
|
self.project,
|
||||||
|
self.domain,
|
||||||
|
self.device_type,
|
||||||
|
self.device_id,
|
||||||
|
self.resource,
|
||||||
|
]
|
||||||
|
|
||||||
|
if any(p in (None, "+") for p in parts):
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot build strict topic, wildcard exists: {self.to_topic()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "/".join(parts)
|
||||||
|
|
||||||
|
def is_wildcard(self) -> bool:
|
||||||
|
return "+" in self.to_topic() or "#" in self.to_topic()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<MqttTopic {self.to_topic()}>"
|
||||||
|
|
||||||
|
def is_status(self) -> bool:
|
||||||
|
return self.domain == "status"
|
||||||
|
|
||||||
|
def is_cmd(self) -> bool:
|
||||||
|
return self.domain == "cmd"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class SentinelRecordRequest(BaseModel):
|
||||||
|
Id: str | None = None
|
||||||
|
DeviceId: str
|
||||||
|
LicensePlate: str | None = None
|
||||||
|
LicensePlateImage: str | None = None
|
||||||
|
VehicleType: str | None = None
|
||||||
|
VehicleImage: str | None = None
|
||||||
+138
-5
@@ -1,17 +1,19 @@
|
|||||||
|
import uuid
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
from config.redis import RedisClient
|
from config.emqx import mqtt_publish
|
||||||
|
from config.minIO import get_upload_token
|
||||||
|
from config.redis import redis_client
|
||||||
from db.postgres.iot import *
|
from db.postgres.iot import *
|
||||||
from models.BaseResponse import BaseResponse
|
from models.BaseResponse import BaseResponse
|
||||||
from models.EMQXWebhook import EMQXWebhook
|
from models.EMQXWebhook import EMQXWebhook
|
||||||
|
from models.IotDeviceCommandRequest import IotDeviceCommandRequest
|
||||||
from routers.WS import ws_manager
|
from routers.WS import ws_manager
|
||||||
|
|
||||||
iot_router = APIRouter()
|
iot_router = APIRouter()
|
||||||
redis_client = RedisClient()
|
|
||||||
|
|
||||||
from config.security import get_user_id_from_token
|
from config.security import get_user_id_from_token
|
||||||
|
|
||||||
# -------------------- 设备接口 --------------------
|
# -------------------- 设备接口 --------------------
|
||||||
@@ -25,14 +27,14 @@ async def emqx_webhook(data: EMQXWebhook):
|
|||||||
if event == "client.connected":
|
if event == "client.connected":
|
||||||
redis_client.set_online(device_id)
|
redis_client.set_online(device_id)
|
||||||
|
|
||||||
await ws_manager.broadcast({"deviceId": device_id, "online": True})
|
await ws_manager.noticeOnlineStatus({"deviceId": device_id, "online": True})
|
||||||
|
|
||||||
print(f"[ONLINE] {device_id}")
|
print(f"[ONLINE] {device_id}")
|
||||||
|
|
||||||
elif event == "client.disconnected":
|
elif event == "client.disconnected":
|
||||||
redis_client.set_offline(device_id)
|
redis_client.set_offline(device_id)
|
||||||
|
|
||||||
await ws_manager.broadcast({"deviceId": device_id, "online": False})
|
await ws_manager.noticeOnlineStatus({"deviceId": device_id, "online": False})
|
||||||
|
|
||||||
print(f"[OFFLINE] {device_id}")
|
print(f"[OFFLINE] {device_id}")
|
||||||
|
|
||||||
@@ -68,6 +70,19 @@ async def get_device_list(
|
|||||||
device_id = d["name"] # 账号
|
device_id = d["name"] # 账号
|
||||||
d["online"] = redis_client.is_device_online(device_id) == 1
|
d["online"] = redis_client.is_device_online(device_id) == 1
|
||||||
|
|
||||||
|
info_json = redis_client.get_device_info(device_id)
|
||||||
|
d["version"] = info_json.get("version", "")
|
||||||
|
d["ip"] = info_json.get("ip", "")
|
||||||
|
d["hostname"] = info_json.get("hostname", "")
|
||||||
|
d["mac"] = info_json.get("mac", "")
|
||||||
|
d["os"] = info_json.get("os", "")
|
||||||
|
d["cpu"] = info_json.get("cpu", "")
|
||||||
|
d["memory_total"] = info_json.get("memory_total", "")
|
||||||
|
d["disk_total"] = info_json.get("disk_total", "")
|
||||||
|
d["last_seen"] = info_json.get("last_seen", "")
|
||||||
|
d["project"] = info_json.get("project", "")
|
||||||
|
d["device_type"] = info_json.get("deviceType", "")
|
||||||
|
|
||||||
return BaseResponse(data={"list": devices, "total": total})
|
return BaseResponse(data={"list": devices, "total": total})
|
||||||
|
|
||||||
|
|
||||||
@@ -121,3 +136,121 @@ async def delete_device(
|
|||||||
if deleted == 0:
|
if deleted == 0:
|
||||||
return BaseResponse(status=False, message="设备不存在", data=None)
|
return BaseResponse(status=False, message="设备不存在", data=None)
|
||||||
return BaseResponse(data=True)
|
return BaseResponse(data=True)
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.get("/common/update/list")
|
||||||
|
async def get_update_list(
|
||||||
|
page: int = 1,
|
||||||
|
pageSize: int = 10,
|
||||||
|
id: str | None = None,
|
||||||
|
code: str | None = None,
|
||||||
|
dept_id: str | None = None,
|
||||||
|
startTime: str | None = None,
|
||||||
|
endTime: str | None = None,
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
if code == "" or code is None:
|
||||||
|
code = None
|
||||||
|
else:
|
||||||
|
code = int(code)
|
||||||
|
|
||||||
|
updates, total = get_update_list_db_page(
|
||||||
|
page, pageSize, id, code, dept_id, startTime, endTime
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse(data={"list": updates, "total": total})
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.post("/common/update")
|
||||||
|
async def create_update(data: dict, user_id: UUID = Depends(get_user_id_from_token)):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
|
||||||
|
dept_id = data.get("dept_id")
|
||||||
|
if not dept_id:
|
||||||
|
return {"error": "dept_id is required"}
|
||||||
|
|
||||||
|
# 前端传来的版本号
|
||||||
|
try:
|
||||||
|
new_code = int(data.get("code", 0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return BaseResponse(
|
||||||
|
status=False,
|
||||||
|
message="无效的版本号",
|
||||||
|
data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取该组织当前最大版本号
|
||||||
|
max_code = getMaxCodeByDeptId(dept_id)
|
||||||
|
|
||||||
|
if new_code <= max_code:
|
||||||
|
return BaseResponse(
|
||||||
|
status=False,
|
||||||
|
message=f"新版本号必须大于当前最大版本号 {max_code}",
|
||||||
|
data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 插入数据库
|
||||||
|
new_id = insert_update(data)
|
||||||
|
return BaseResponse(data={"id": new_id})
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.delete("/common/update/{id}")
|
||||||
|
async def delete_update(
|
||||||
|
id: str,
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
|
||||||
|
deleted = delete_update_db(id)
|
||||||
|
if deleted == 0:
|
||||||
|
return BaseResponse(status=False, message="更新记录不存在", data=None)
|
||||||
|
|
||||||
|
return BaseResponse(data=True)
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.get("/common/update/getUploadUrl")
|
||||||
|
def getUploadUrl(
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
):
|
||||||
|
# 生成唯一文件名,避免覆盖
|
||||||
|
object_name = f"{uuid.uuid4()}"
|
||||||
|
return BaseResponse(
|
||||||
|
data={
|
||||||
|
"uploadUrl": get_upload_token("iot-update", object_name),
|
||||||
|
"id": object_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.get("/common/update/getMaxCodeByDeptId")
|
||||||
|
def updateGetMaxCodeByDeptId(
|
||||||
|
user_id: UUID = Depends(get_user_id_from_token),
|
||||||
|
dept_id: str | None = None,
|
||||||
|
):
|
||||||
|
# 生成唯一文件名,避免覆盖
|
||||||
|
return BaseResponse(data=getMaxCodeByDeptId(dept_id))
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.get("/common/update/check")
|
||||||
|
def getUploadUrl(
|
||||||
|
deviceID: str | None = None,
|
||||||
|
):
|
||||||
|
# 生成唯一文件名,避免覆盖
|
||||||
|
return BaseResponse(data=get_update_package(deviceID))
|
||||||
|
|
||||||
|
|
||||||
|
@iot_router.post("/common/device/command")
|
||||||
|
async def command(
|
||||||
|
data: IotDeviceCommandRequest, user_id: UUID = Depends(get_user_id_from_token)
|
||||||
|
):
|
||||||
|
if not user_id:
|
||||||
|
return {"error": "userId is required"}
|
||||||
|
|
||||||
|
await mqtt_publish(
|
||||||
|
data.project, "cmd", data.device_type, data.id, data.command, "{}"
|
||||||
|
)
|
||||||
|
return BaseResponse(data=None)
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import base64
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from config.app import F8_SERVER_USER_ID
|
from config.app import F8_SERVER_USER_ID
|
||||||
|
from db.postgres.sentinel import saveSentinelRecord
|
||||||
from models.BaseResponse import BaseResponse
|
from models.BaseResponse import BaseResponse
|
||||||
from models.F8ImageRequest import F8ImageRequest
|
from models.F8ImageRequest import F8ImageRequest
|
||||||
from models.F8ImageRequestV2 import F8ImageRequestV2
|
from models.F8ImageRequestV2 import F8ImageRequestV2
|
||||||
|
from models.SentinelRecordRequest import SentinelRecordRequest
|
||||||
|
from service.RabbitMQ import sentinel_new_analysis
|
||||||
from service.vision import (
|
from service.vision import (
|
||||||
process_ticket_image,
|
process_ticket_image,
|
||||||
process_license_image,
|
process_license_image,
|
||||||
@@ -78,3 +81,12 @@ async def recognize_silkworm_cocoon(data: F8ImageRequest):
|
|||||||
return BaseResponse(data=json_data)
|
return BaseResponse(data=json_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||||
|
|
||||||
|
|
||||||
|
@publicRouter.post("/sentinel-record-analytics")
|
||||||
|
async def delete_sentinel_record(data: SentinelRecordRequest):
|
||||||
|
# 保存部分数据到数据库
|
||||||
|
data.Id = saveSentinelRecord(data)
|
||||||
|
# 发送请求给RabbitMQ
|
||||||
|
res = await sentinel_new_analysis(data)
|
||||||
|
return BaseResponse(data=res)
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from models.AnalysisRequest import AnalysisRequest
|
|
||||||
from service.Analyze import mq_new_analysis
|
|
||||||
|
|
||||||
rqRouter = APIRouter()
|
rqRouter = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@rqRouter.post("/analyze")
|
# @rqRouter.post("/analyze")
|
||||||
def send_analysis_request(req: AnalysisRequest):
|
# def send_analysis_request(req: AnalysisRequest):
|
||||||
mq_new_analysis(req)
|
# mq_new_analysis(req)
|
||||||
return {"status": "queued"}
|
# return {"status": "queued"}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ serviceRouter = APIRouter()
|
|||||||
|
|
||||||
# 对话列表
|
# 对话列表
|
||||||
@serviceRouter.get("/sessionsForService")
|
@serviceRouter.get("/sessionsForService")
|
||||||
def getSessions(user_id: UUID = Depends(get_user_id_from_token)):
|
async def getSessions(user_id: UUID = Depends(get_user_id_from_token)):
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return {"error": "userId is required"}
|
return {"error": "userId is required"}
|
||||||
return BaseResponse(data=pg.get_sessions(user_id, "service"))
|
return BaseResponse(data=pg.get_sessions(user_id, "service"))
|
||||||
@@ -25,7 +25,7 @@ def getSessions(user_id: UUID = Depends(get_user_id_from_token)):
|
|||||||
|
|
||||||
# 对话
|
# 对话
|
||||||
@serviceRouter.post("/chatForService")
|
@serviceRouter.post("/chatForService")
|
||||||
def chat(req: ChatRequest, user_id: UUID = Depends(get_user_id_from_token)):
|
async def chat(req: ChatRequest, user_id: UUID = Depends(get_user_id_from_token)):
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return {"error": "userId is required"}
|
return {"error": "userId is required"}
|
||||||
if not req.aiId:
|
if not req.aiId:
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ async def menu_list(plat_id: int, user_id: UUID = Depends(get_user_id_from_token
|
|||||||
m["createTime"] = format_datetime(m.get("created_at"))
|
m["createTime"] = format_datetime(m.get("created_at"))
|
||||||
m["updateTime"] = format_datetime(m.get("updated_at"))
|
m["updateTime"] = format_datetime(m.get("updated_at"))
|
||||||
m["children"] = []
|
m["children"] = []
|
||||||
|
# 删除created_at updated_at
|
||||||
|
m.pop("createTime", None)
|
||||||
|
m.pop("updateTime", None)
|
||||||
|
|
||||||
# 5. 构建菜单树
|
# 5. 构建菜单树
|
||||||
tree = build_menu_tree(menus)
|
tree = build_menu_tree(menus)
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ def getIVASCUploadToken(
|
|||||||
):
|
):
|
||||||
# 生成唯一文件名,避免覆盖
|
# 生成唯一文件名,避免覆盖
|
||||||
object_name = f"raw/{uuid.uuid4()}"
|
object_name = f"raw/{uuid.uuid4()}"
|
||||||
return BaseResponse(data=get_upload_token(user_id, "video-sca", object_name))
|
return BaseResponse(data=get_upload_token("video-sca", object_name))
|
||||||
|
|
||||||
|
|
||||||
@visionRouter.get("/getScVideoList")
|
@visionRouter.get("/getScVideoList")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Query
|
||||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||||
|
|
||||||
|
from config.security import get_user_id_from_token_from_ws
|
||||||
|
from db.postgres import get_dept_id_by_user_id
|
||||||
from db.postgres.ws_manager import ConnectionManager
|
from db.postgres.ws_manager import ConnectionManager
|
||||||
|
|
||||||
ws_manager = ConnectionManager()
|
ws_manager = ConnectionManager()
|
||||||
@@ -10,8 +12,13 @@ iot_ws_router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@iot_ws_router.websocket("/device-status")
|
@iot_ws_router.websocket("/device-status")
|
||||||
async def websocket_device_status(websocket: WebSocket):
|
async def websocket_device_status(
|
||||||
await ws_manager.connect(websocket)
|
websocket: WebSocket,
|
||||||
|
token: str = Query(...),
|
||||||
|
):
|
||||||
|
user_id = get_user_id_from_token_from_ws(token)
|
||||||
|
dept_id = get_dept_id_by_user_id(user_id) # 查数据库或缓存
|
||||||
|
await ws_manager.connect(websocket, user_id, dept_id, 0)
|
||||||
print("[WS] client connected")
|
print("[WS] client connected")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -21,3 +28,22 @@ async def websocket_device_status(websocket: WebSocket):
|
|||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
await ws_manager.disconnect(websocket)
|
await ws_manager.disconnect(websocket)
|
||||||
print("[WS] client disconnected")
|
print("[WS] client disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
@iot_ws_router.websocket("/sentinel_record")
|
||||||
|
async def websocket_sentinel_record(
|
||||||
|
websocket: WebSocket,
|
||||||
|
token: str = Query(...),
|
||||||
|
):
|
||||||
|
user_id = get_user_id_from_token_from_ws(token)
|
||||||
|
dept_id = get_dept_id_by_user_id(user_id) # 查数据库或缓存
|
||||||
|
print("user_id:", user_id)
|
||||||
|
print("dept_id:", dept_id)
|
||||||
|
print("已接入")
|
||||||
|
await ws_manager.connect(websocket, user_id, dept_id, 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
await ws_manager.disconnect(websocket)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# consumer.py
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
import aio_pika
|
||||||
|
|
||||||
|
from config.rabbitMQ import *
|
||||||
|
from models.AnalysisRequest import AnalysisRequest
|
||||||
|
from models.SentinelRecordRequest import SentinelRecordRequest
|
||||||
|
from service.vision import process_vehicle_animal_image
|
||||||
|
|
||||||
|
|
||||||
|
async def mq_new_analysis_test(req: dict):
|
||||||
|
"""将分析请求发送到 RabbitMQ 队列(异步版)"""
|
||||||
|
connection = await aio_pika.connect_robust(
|
||||||
|
f"amqp://{RABBIT_USER}:{RABBIT_PASSWORD}@{RABBIT_HOST}/{RABBIT_VHOST}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with connection:
|
||||||
|
channel = await connection.channel()
|
||||||
|
# 声明队列,确保队列存在
|
||||||
|
queue = await channel.declare_queue(QUEUE_NAME, durable=True)
|
||||||
|
|
||||||
|
message_body = json.dumps(req)
|
||||||
|
message = aio_pika.Message(
|
||||||
|
body=message_body.encode(),
|
||||||
|
delivery_mode=aio_pika.DeliveryMode.PERSISTENT, # 持久化
|
||||||
|
)
|
||||||
|
|
||||||
|
await channel.default_exchange.publish(message, routing_key=QUEUE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
async def mq_pull_analysis_async_test():
|
||||||
|
"""
|
||||||
|
从队列拉取分析任务并处理
|
||||||
|
process_func: 一个函数,接收 AnalysisRequest 对象处理分析逻辑
|
||||||
|
"""
|
||||||
|
connection = await aio_pika.connect_robust(
|
||||||
|
f"amqp://{RABBIT_USER}:{RABBIT_PASSWORD}@{RABBIT_HOST}/{RABBIT_VHOST}"
|
||||||
|
)
|
||||||
|
async with connection:
|
||||||
|
queue_name = QUEUE_NAME
|
||||||
|
channel = await connection.channel()
|
||||||
|
await channel.set_qos(prefetch_count=1)
|
||||||
|
queue = await channel.declare_queue(queue_name, durable=True)
|
||||||
|
|
||||||
|
async with queue.iterator() as queue_iter:
|
||||||
|
async for message in queue_iter:
|
||||||
|
async with message.process():
|
||||||
|
data = json.loads(message.body)
|
||||||
|
req = AnalysisRequest(**data)
|
||||||
|
print(f"收到任务: {req}")
|
||||||
|
await asyncio.sleep(5) # 模拟处理
|
||||||
|
print(f"完成任务: {req}")
|
||||||
|
|
||||||
|
|
||||||
|
async def sentinel_new_analysis(req: SentinelRecordRequest):
|
||||||
|
"""将分析请求发送到 RabbitMQ 队列(异步版)"""
|
||||||
|
connection = await aio_pika.connect_robust(
|
||||||
|
f"amqp://{RABBIT_USER}:{RABBIT_PASSWORD}@{RABBIT_HOST}/{SENTINEL_VHOST}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with connection:
|
||||||
|
channel = await connection.channel()
|
||||||
|
# 声明队列,确保队列存在
|
||||||
|
queue = await channel.declare_queue(QUEUE_NAME, durable=True)
|
||||||
|
|
||||||
|
message_body = json.dumps(req.model_dump())
|
||||||
|
message = aio_pika.Message(
|
||||||
|
body=message_body.encode(),
|
||||||
|
delivery_mode=aio_pika.DeliveryMode.PERSISTENT, # 持久化
|
||||||
|
)
|
||||||
|
|
||||||
|
await channel.default_exchange.publish(message, routing_key=QUEUE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
async def sentinel_pull_analysis_async():
|
||||||
|
"""
|
||||||
|
从队列拉取分析任务并处理
|
||||||
|
process_func: 一个函数,接收 AnalysisRequest 对象处理分析逻辑
|
||||||
|
"""
|
||||||
|
connection = await aio_pika.connect_robust(
|
||||||
|
f"amqp://{RABBIT_USER}:{RABBIT_PASSWORD}@{RABBIT_HOST}/{SENTINEL_VHOST}"
|
||||||
|
)
|
||||||
|
async with connection:
|
||||||
|
queue_name = QUEUE_NAME
|
||||||
|
channel = await connection.channel()
|
||||||
|
await channel.set_qos(prefetch_count=1)
|
||||||
|
queue = await channel.declare_queue(queue_name, durable=True)
|
||||||
|
|
||||||
|
async with queue.iterator() as queue_iter:
|
||||||
|
async for message in queue_iter:
|
||||||
|
async with message.process():
|
||||||
|
data = json.loads(message.body)
|
||||||
|
req = SentinelRecordRequest(**data)
|
||||||
|
print(f"收到任务: {req}")
|
||||||
|
await process_vehicle_animal_image(req) # 处理
|
||||||
|
print(f"完成任务: {req}")
|
||||||
@@ -4,10 +4,15 @@ from uuid import UUID
|
|||||||
import config.minIO as minIO
|
import config.minIO as minIO
|
||||||
import db.postgres as pg
|
import db.postgres as pg
|
||||||
from agent.licenseImageAgent import get_license_response
|
from agent.licenseImageAgent import get_license_response
|
||||||
|
from agent.vehicleImageAgent import get_vehicle_response
|
||||||
from config.minIO import minio_client
|
from config.minIO import minio_client
|
||||||
from config.yolo import YOLOSingleton
|
from config.yolo import YOLOSingleton
|
||||||
|
from db.postgres import get_dept_id_by_iot_user_name, get_dept_ids_by_dept_id
|
||||||
|
from db.postgres.sentinel import update_sentinel_record
|
||||||
from llm.ticketLLM import *
|
from llm.ticketLLM import *
|
||||||
from llm.ticketLLMv2 import get_ticket_response_v2
|
from llm.ticketLLMv2 import get_ticket_response_v2
|
||||||
|
from models.SentinelRecordRequest import SentinelRecordRequest
|
||||||
|
from routers.WS import ws_manager
|
||||||
|
|
||||||
|
|
||||||
def process_ticket_image(
|
def process_ticket_image(
|
||||||
@@ -178,3 +183,27 @@ def process_silkworm_cocoon_image(
|
|||||||
"postprocess_time_ms": speed_json.get("postprocess"),
|
"postprocess_time_ms": speed_json.get("postprocess"),
|
||||||
"details": results_json.get("class_counts"),
|
"details": results_json.get("class_counts"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def process_vehicle_animal_image(
|
||||||
|
data: SentinelRecordRequest,
|
||||||
|
):
|
||||||
|
# 通过设备id获得组织id
|
||||||
|
dept_id = get_dept_id_by_iot_user_name(data.DeviceId)
|
||||||
|
# 得到动物类型
|
||||||
|
oss_url = minIO.get_temp_url("sentinel", "vehicle_image/" + data.VehicleImage)
|
||||||
|
analysis_result = await get_vehicle_response(oss_url)
|
||||||
|
livestock_type = analysis_result.get("livestock_type", "")
|
||||||
|
remark = analysis_result.get("remark", "")
|
||||||
|
|
||||||
|
available_departments = get_dept_ids_by_dept_id(dept_id)
|
||||||
|
|
||||||
|
await ws_manager.noticeSentinel(
|
||||||
|
{
|
||||||
|
"content": f"载有{livestock_type}的车辆即将进入关卡,请准备检查",
|
||||||
|
"type": "vehicle_alert",
|
||||||
|
},
|
||||||
|
available_departments,
|
||||||
|
)
|
||||||
|
# 保存到数据库
|
||||||
|
return update_sentinel_record(data.Id, livestock_type, remark, dept_id)
|
||||||
|
|||||||
Reference in New Issue
Block a user