完善牧安云哨-后端

This commit is contained in:
BBIT-Kai
2025-12-29 16:30:36 +08:00
parent cd7aa35960
commit b9b8d30ebf
23 changed files with 1074 additions and 41 deletions
+138 -5
View File
@@ -1,17 +1,19 @@
import uuid
from uuid import UUID
from fastapi import APIRouter
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 models.BaseResponse import BaseResponse
from models.EMQXWebhook import EMQXWebhook
from models.IotDeviceCommandRequest import IotDeviceCommandRequest
from routers.WS import ws_manager
iot_router = APIRouter()
redis_client = RedisClient()
from config.security import get_user_id_from_token
# -------------------- 设备接口 --------------------
@@ -25,14 +27,14 @@ async def emqx_webhook(data: EMQXWebhook):
if event == "client.connected":
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}")
elif event == "client.disconnected":
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}")
@@ -68,6 +70,19 @@ async def get_device_list(
device_id = d["name"] # 账号
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})
@@ -121,3 +136,121 @@ async def delete_device(
if deleted == 0:
return BaseResponse(status=False, message="设备不存在", data=None)
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)
+12
View File
@@ -3,9 +3,12 @@ import base64
from fastapi import APIRouter
from config.app import F8_SERVER_USER_ID
from db.postgres.sentinel import saveSentinelRecord
from models.BaseResponse import BaseResponse
from models.F8ImageRequest import F8ImageRequest
from models.F8ImageRequestV2 import F8ImageRequestV2
from models.SentinelRecordRequest import SentinelRecordRequest
from service.RabbitMQ import sentinel_new_analysis
from service.vision import (
process_ticket_image,
process_license_image,
@@ -78,3 +81,12 @@ async def recognize_silkworm_cocoon(data: F8ImageRequest):
return BaseResponse(data=json_data)
except Exception as e:
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)
+4 -7
View File
@@ -1,12 +1,9 @@
from fastapi import APIRouter
from models.AnalysisRequest import AnalysisRequest
from service.Analyze import mq_new_analysis
rqRouter = APIRouter()
@rqRouter.post("/analyze")
def send_analysis_request(req: AnalysisRequest):
mq_new_analysis(req)
return {"status": "queued"}
# @rqRouter.post("/analyze")
# def send_analysis_request(req: AnalysisRequest):
# mq_new_analysis(req)
# return {"status": "queued"}
+2 -2
View File
@@ -17,7 +17,7 @@ serviceRouter = APIRouter()
# 对话列表
@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:
return {"error": "userId is required"}
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")
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:
return {"error": "userId is required"}
if not req.aiId:
+3
View File
@@ -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["updateTime"] = format_datetime(m.get("updated_at"))
m["children"] = []
# 删除created_at updated_at
m.pop("createTime", None)
m.pop("updateTime", None)
# 5. 构建菜单树
tree = build_menu_tree(menus)
+1 -1
View File
@@ -162,7 +162,7 @@ def getIVASCUploadToken(
):
# 生成唯一文件名,避免覆盖
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")
+29 -3
View File
@@ -1,6 +1,8 @@
from fastapi import APIRouter
from fastapi import APIRouter, Query
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
ws_manager = ConnectionManager()
@@ -10,8 +12,13 @@ iot_ws_router = APIRouter()
@iot_ws_router.websocket("/device-status")
async def websocket_device_status(websocket: WebSocket):
await ws_manager.connect(websocket)
async def websocket_device_status(
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")
try:
@@ -21,3 +28,22 @@ async def websocket_device_status(websocket: WebSocket):
except WebSocketDisconnect:
await ws_manager.disconnect(websocket)
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)