完善牧安云哨-后端

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
+211
View File
@@ -1,5 +1,6 @@
from hashlib import sha256
from config.minIO import get_temp_url
from config.pgDb import pg_pool
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,))
conn.commit()
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
+53
View File
@@ -1,5 +1,6 @@
from config.minIO import get_temp_url_dict
from config.pgDb import pg_pool
from models.SentinelRecordRequest import SentinelRecordRequest
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,))
conn.commit()
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
+46 -2
View File
@@ -942,11 +942,55 @@ def get_dept_ids_by_user_id(user_id: UUID) -> list:
return dept_ids
def get_dept_id_by_user_id(user_id: UUID) -> list:
# 第一步:通过 user_id 查找其所属的 dept_id
def get_dept_id_by_user_id(user_id: str) -> str:
# 通过 user_id 查找其所属的 dept_id
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT dept_id FROM users WHERE id = %s", (user_id,))
dept_id = cursor.fetchone()
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
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]
+33 -8
View File
@@ -1,25 +1,50 @@
import asyncio
from typing import List
from uuid import UUID
from fastapi import WebSocket
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
self.active_connections: List[dict] = [] # 保存 websocket 和用户信息
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()
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 with self.lock:
if websocket in self.active_connections:
self.active_connections.remove(websocket)
self.active_connections = [
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:
for ws in self.active_connections:
await ws.send_json(message)
for conn in self.active_connections:
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)