AI实验室后端
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import pathlib
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends, APIRouter
|
||||
|
||||
import db.postgres as pg
|
||||
from config.minIO import get_upload_token
|
||||
from config.security import get_user_id_from_token
|
||||
from models.BaseResponse import BaseResponse
|
||||
from models.LotteryCreateReq import LotteryCreateReq
|
||||
from models.LotteryUpdateReq import LotteryUpdateReq
|
||||
|
||||
amRouter = APIRouter()
|
||||
|
||||
|
||||
@amRouter.get("/ExGetList")
|
||||
def AmExGetList(user_id: UUID = Depends(get_user_id_from_token)):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
data = pg.get_all_exchange_records()
|
||||
return BaseResponse(data=data)
|
||||
|
||||
|
||||
@amRouter.get("/ExReset")
|
||||
def AMExReset(user_id: UUID = Depends(get_user_id_from_token)):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
data = pg.reset_all_exchange_status()
|
||||
return BaseResponse(data=data, message="已重新打乱顺序")
|
||||
|
||||
|
||||
@amRouter.put("/ExResetTargetStatus")
|
||||
def AMExResetTargetStatus(
|
||||
target_user_id: str, user_id: UUID = Depends(get_user_id_from_token)
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
data = pg.reset_user_status(target_user_id)
|
||||
return BaseResponse(data=data)
|
||||
|
||||
|
||||
@amRouter.get("/Lottery/getUploadUrl")
|
||||
def getUploadUrl(
|
||||
filename: str | None = None,
|
||||
user_id: UUID = Depends(get_user_id_from_token),
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
# 生成唯一文件名,避免覆盖
|
||||
ext = pathlib.Path(filename).suffix if filename else "" # 获取文件后缀
|
||||
object_name = f"{uuid.uuid4()}{ext}" # 拼接到 UUID 后面
|
||||
return BaseResponse(
|
||||
data={
|
||||
"uploadUrl": get_upload_token("image-annual-lottery", object_name),
|
||||
"id": object_name,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@amRouter.get("/Lottery/List")
|
||||
def AMGetLotteryList(user_id: UUID = Depends(get_user_id_from_token)):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
data = pg.get_all_lottery()
|
||||
return BaseResponse(data=data)
|
||||
|
||||
|
||||
@amRouter.post("/Lottery/Add")
|
||||
def AMAddLottery(
|
||||
data: LotteryCreateReq,
|
||||
user_id: UUID = Depends(get_user_id_from_token),
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
new_data = pg.add_lottery(
|
||||
name=data.name,
|
||||
sort=data.sort,
|
||||
oss=data.oss,
|
||||
is_opened=data.is_opened,
|
||||
remark=data.remark,
|
||||
)
|
||||
return BaseResponse(data=new_data)
|
||||
|
||||
|
||||
@amRouter.put("/Lottery/Update")
|
||||
def AMUpdateLottery(
|
||||
data: LotteryUpdateReq,
|
||||
user_id: UUID = Depends(get_user_id_from_token),
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
|
||||
updated = pg.update_lottery(
|
||||
id=data.id,
|
||||
name=data.name,
|
||||
sort=data.sort,
|
||||
oss=data.oss,
|
||||
is_opened=data.is_opened,
|
||||
remark=data.remark,
|
||||
)
|
||||
return BaseResponse(data=updated)
|
||||
|
||||
|
||||
@amRouter.delete("/Lottery/Delete")
|
||||
def AMDeleteLottery(id: str, user_id: UUID = Depends(get_user_id_from_token)):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
data = pg.delete_lottery(id)
|
||||
return BaseResponse(data=data)
|
||||
|
||||
|
||||
@amRouter.patch("/Lottery/open/{item_id}")
|
||||
def open_lottery_item(item_id: UUID):
|
||||
updated = pg.reset_lottery_item(item_id)
|
||||
return BaseResponse(data=updated)
|
||||
|
||||
|
||||
@amRouter.patch("/Lottery/resetAll")
|
||||
def reset_all_lottery():
|
||||
count = pg.reset_all_lottery_db()
|
||||
return BaseResponse(data=count)
|
||||
+71
-43
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import json
|
||||
import pathlib
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
@@ -6,14 +7,11 @@ from uuid import UUID
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
|
||||
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()
|
||||
from config.security import get_user_id_from_token
|
||||
@@ -21,43 +19,44 @@ from config.security import get_user_id_from_token
|
||||
# -------------------- 设备接口 --------------------
|
||||
|
||||
|
||||
@iot_router.post("/common/webhook")
|
||||
async def emqx_webhook(data: EMQXWebhook):
|
||||
device_id = data.clientid
|
||||
event = data.event
|
||||
|
||||
if event == "client.connected":
|
||||
redis_client.set_online(device_id)
|
||||
# 这里刻意等1s 是因为设备连接后这里首先接到通知,但是状态信息设备来没来得及通过mqtt发送来,所以在此等待
|
||||
# 没有直接在mqtt发送来的消息中获取在线状态是因为 这里是通过emqx的webhooks通知的,两种通知方式不同,一方面防止其中一种逻辑失效,另一方面在mqtt消息接收中设置在线状态会存在滞后性,同时也需要设置遗嘱消息,较为
|
||||
await asyncio.sleep(1)
|
||||
await ws_manager.noticeOnlineStatus(
|
||||
{
|
||||
"deviceId": device_id,
|
||||
"online": True,
|
||||
"type": "status",
|
||||
}
|
||||
)
|
||||
|
||||
print(f"[新设备在线] {device_id}")
|
||||
|
||||
elif event == "client.disconnected":
|
||||
redis_client.set_offline(device_id)
|
||||
await ws_manager.noticeOnlineStatus(
|
||||
{
|
||||
"deviceId": device_id,
|
||||
"online": False,
|
||||
"type": "status",
|
||||
}
|
||||
)
|
||||
|
||||
print(f"[设备离线] {device_id}")
|
||||
|
||||
else:
|
||||
# 其他事件直接忽略
|
||||
print(f"[其他事件] {event}")
|
||||
|
||||
return {"ok": True}
|
||||
# 已废弃 Webhooks的离线通知不及时(突然断电断网)
|
||||
# @iot_router.post("/common/webhook")
|
||||
# async def emqx_webhook(data: EMQXWebhook):
|
||||
# device_id = data.clientid
|
||||
# event = data.event
|
||||
#
|
||||
# if event == "client.connected":
|
||||
# redis_client.set_online(device_id)
|
||||
# # 这里刻意等1s 是因为设备连接后这里首先接到通知,但是状态信息设备来没来得及通过mqtt发送来,所以在此等待
|
||||
# # 没有直接在mqtt发送来的消息中获取在线状态是因为 这里是通过emqx的webhooks通知的,两种通知方式不同,一方面防止其中一种逻辑失效,另一方面在mqtt消息接收中设置在线状态会存在滞后性,同时也需要设置遗嘱消息,较为
|
||||
# await asyncio.sleep(1)
|
||||
# await ws_manager.noticeOnlineStatus(
|
||||
# {
|
||||
# "deviceId": device_id,
|
||||
# "online": True,
|
||||
# "type": "status",
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# print(f"[新设备在线] {device_id}")
|
||||
#
|
||||
# elif event == "client.disconnected":
|
||||
# redis_client.set_offline(device_id)
|
||||
# await ws_manager.noticeOnlineStatus(
|
||||
# {
|
||||
# "deviceId": device_id,
|
||||
# "online": False,
|
||||
# "type": "status",
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# print(f"[设备离线] {device_id}")
|
||||
#
|
||||
# else:
|
||||
# # 其他事件直接忽略
|
||||
# print(f"[其他事件] {event}")
|
||||
#
|
||||
# return {"ok": True}
|
||||
|
||||
|
||||
@iot_router.get("/common/device/list")
|
||||
@@ -83,9 +82,9 @@ async def get_device_list(
|
||||
# ===== 👇 核心:补在线状态 =====
|
||||
for d in devices:
|
||||
device_id = d["name"] # 账号
|
||||
d["online"] = redis_client.is_device_online(device_id) == 1
|
||||
|
||||
info_json = redis_client.get_device_info(device_id)
|
||||
d["online"] = info_json.get("online", "0") == "1"
|
||||
d["version"] = info_json.get("version", "")
|
||||
d["ip"] = info_json.get("ip", "")
|
||||
d["hostname"] = info_json.get("hostname", "")
|
||||
@@ -261,6 +260,10 @@ def getUploadUrl(
|
||||
return BaseResponse(data=get_update_package(deviceID))
|
||||
|
||||
|
||||
# request_id -> asyncio.Future
|
||||
pending_commands: dict[str, asyncio.Future] = {}
|
||||
|
||||
|
||||
@iot_router.post("/common/device/command")
|
||||
async def command(
|
||||
data: IotDeviceCommandRequest, user_id: UUID = Depends(get_user_id_from_token)
|
||||
@@ -268,7 +271,32 @@ async def command(
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
payload = {"request_id": request_id}
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
future = loop.create_future()
|
||||
pending_commands[request_id] = future
|
||||
|
||||
from config.emqx import mqtt_publish
|
||||
|
||||
await mqtt_publish(
|
||||
data.dept_id, "cmd", data.device_type, data.id, data.command, "{}"
|
||||
data.dept_id,
|
||||
"cmd",
|
||||
data.device_type,
|
||||
data.id,
|
||||
data.command,
|
||||
json.dumps(payload),
|
||||
)
|
||||
return BaseResponse(data=None)
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(future, timeout=5)
|
||||
return BaseResponse(data=result.get("massage"))
|
||||
except asyncio.TimeoutError:
|
||||
return BaseResponse(data=None, message="Device did not respond in time")
|
||||
except asyncio.CancelledError:
|
||||
# 请求被中断,必须清理,但不要吞
|
||||
pending_commands.pop(request_id, None)
|
||||
raise
|
||||
finally:
|
||||
pending_commands.pop(request_id, None)
|
||||
|
||||
@@ -2,6 +2,9 @@ from uuid import UUID
|
||||
|
||||
from fastapi import Depends, APIRouter
|
||||
|
||||
from config.httpClient import HttpClient
|
||||
from config.minIO import get_temp_url
|
||||
from config.redis import redis_client
|
||||
from config.security import get_user_id_from_token
|
||||
from db.postgres import get_dept_ids_by_user_id, get_dept_id_by_user_id
|
||||
from db.postgres.sentinel import *
|
||||
@@ -96,3 +99,94 @@ async def delete_sentinel_record(
|
||||
if deleted == 0:
|
||||
return BaseResponse(status=False, message="记录不存在", data=None)
|
||||
return BaseResponse(data=True)
|
||||
|
||||
|
||||
@sentinel_router.get("/monitor/promotional/list")
|
||||
async def get_sentinel_monitor_promotional_list(
|
||||
user_id: UUID = Depends(get_user_id_from_token),
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
return BaseResponse(
|
||||
data=[
|
||||
{
|
||||
"id": 1,
|
||||
"remark": "人员公示及岗位职责",
|
||||
"url": get_temp_url("sentinel", "promotional/promotional (2).jpg"),
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"remark": "入川动物监督检查工作流程图",
|
||||
"url": get_temp_url("sentinel", "promotional/promotional (1).jpg"),
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"remark": "四川省人民政府关于设立人川动物运输指定通道的通告",
|
||||
"url": get_temp_url("sentinel", "promotional/promotional (3).jpg"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
http_client = HttpClient()
|
||||
|
||||
|
||||
@sentinel_router.get("/monitor/list")
|
||||
async def get_sentinel_monitor_list(
|
||||
user_id: UUID = Depends(get_user_id_from_token),
|
||||
):
|
||||
if not user_id:
|
||||
return {"error": "userId is required"}
|
||||
|
||||
# 尝试从 Redis 获取 accessToken
|
||||
access_token = redis_client.get_value("ys7:access_token")
|
||||
if not access_token:
|
||||
url = "https://open.ys7.com/api/lapp/token/get"
|
||||
payload = {
|
||||
"appKey": "c85e53559223457f90f06cd215513c3d",
|
||||
"appSecret": "9424419da5292707eff2007e9ae37f0d",
|
||||
}
|
||||
result = await http_client.post(url, data=payload)
|
||||
access_token = result["data"]["accessToken"]
|
||||
redis_client.set_value(
|
||||
"ys7:access_token", access_token, expire=7 * 24 * 60 * 60 # 7天过期
|
||||
)
|
||||
|
||||
url = "https://open.ys7.com/api/lapp/v2/live/address/get"
|
||||
# device_serials = ["BG2493625"]
|
||||
device_serials = ["BG2493625", "GH3713250", "GH3714496", "GH3714497"]
|
||||
|
||||
video_expire_time = 25 * 24 * 60 * 60 # 25 天
|
||||
res = []
|
||||
for device_serial in device_serials:
|
||||
live_key = f"ys7:live:{device_serial}"
|
||||
cached_live = redis_client.get_value(live_key)
|
||||
|
||||
if cached_live:
|
||||
video_id = cached_live.get("id")
|
||||
video_url = cached_live.get("url")
|
||||
else:
|
||||
payload = {
|
||||
"accessToken": access_token,
|
||||
"deviceSerial": device_serial,
|
||||
"protocol": 4, # 流播放协议,1-ezopen、2-hls、3-rtmp、4-flv,默认为1
|
||||
"expireTime": video_expire_time, # 25天
|
||||
"supportH265": 0,
|
||||
"quality": 2,
|
||||
}
|
||||
result = await http_client.post(url, data=payload)
|
||||
video_id = result["data"]["id"]
|
||||
video_url = result["data"]["url"]
|
||||
# 存到 Redis,自动序列化为 JSON,过期 25天
|
||||
redis_client.set_value(
|
||||
live_key,
|
||||
{"id": video_id, "url": video_url},
|
||||
expire=video_expire_time,
|
||||
)
|
||||
res.append(
|
||||
{
|
||||
"id": video_id,
|
||||
"url": video_url,
|
||||
}
|
||||
)
|
||||
return BaseResponse(data=res)
|
||||
|
||||
@@ -41,7 +41,7 @@ async def dept_add(data: dict, user_id: UUID = Depends(get_user_id_from_token)):
|
||||
|
||||
parent_id = data.get("pid")
|
||||
name = data.get("name")
|
||||
comment = data.get("comment")
|
||||
comment = data.get("remark")
|
||||
|
||||
if not name:
|
||||
return BaseResponse(status=False, message="部门名不能为空", data=None)
|
||||
|
||||
@@ -127,10 +127,10 @@ async def createSilkwormCocoonAnalysisTask(
|
||||
return {"error": "userId is required"}
|
||||
try:
|
||||
contents = await file.read()
|
||||
await MyUtils.async_task(
|
||||
res = await MyUtils.async_task(
|
||||
process_silkworm_cocoon_image, contents, file.filename, projectName, user_id
|
||||
)
|
||||
return BaseResponse(data=None)
|
||||
return BaseResponse(data=res)
|
||||
except Exception as e:
|
||||
return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None)
|
||||
|
||||
|
||||
@@ -49,3 +49,23 @@ async def websocket_sentinel_record(
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
await ws_manager.disconnect(websocket)
|
||||
|
||||
|
||||
# Vue 牧安云哨 监控大屏 消息通知
|
||||
@iot_ws_router.websocket("/sentinel_record_notice")
|
||||
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, 2)
|
||||
|
||||
try:
|
||||
while True:
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
await ws_manager.disconnect(websocket)
|
||||
|
||||
Reference in New Issue
Block a user