牧安云哨-后端

This commit is contained in:
BBIT-Kai
2025-12-23 09:55:06 +08:00
parent f018d96e60
commit b5a9714025
16 changed files with 1219 additions and 75 deletions
+6
View File
@@ -8,13 +8,16 @@ 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
from routers.Datasource import reportDataRouter from routers.Datasource import reportDataRouter
from routers.Iot import iot_router
from routers.Knowledge import knowledgeRouter from routers.Knowledge import knowledgeRouter
from routers.Public import publicRouter from routers.Public import publicRouter
from routers.RabbitMQ import rqRouter from routers.RabbitMQ import rqRouter
from routers.Report import reportRouter from routers.Report import reportRouter
from routers.Sentinel import sentinel_router
from routers.Service import serviceRouter 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
async def ai_lab(): async def ai_lab():
@@ -46,6 +49,9 @@ async def ai_lab():
app.include_router(r, prefix="/llm", tags=["llm"]) app.include_router(r, prefix="/llm", tags=["llm"])
app.include_router(visionRouter, prefix="/cv", tags=["cv"]) app.include_router(visionRouter, prefix="/cv", tags=["cv"])
app.include_router(systemRouter, prefix="/system", tags=["system"]) app.include_router(systemRouter, prefix="/system", tags=["system"])
app.include_router(iot_router, prefix="/iot", tags=["iot"])
app.include_router(sentinel_router, prefix="/iot/sentinel", tags=["iot_sentinel"])
app.include_router(iot_ws_router, prefix="/iot/ws", tags=["iot_ws"])
app.include_router(publicRouter, prefix="/api/public", tags=["api"]) app.include_router(publicRouter, prefix="/api/public", tags=["api"])
config = Config(app=app, host="0.0.0.0", port=13011, log_level="debug") config = Config(app=app, host="0.0.0.0", port=13011, log_level="debug")
server = Server(config) server = Server(config)
+19
View File
@@ -29,6 +29,25 @@ def get_upload_token(user_id, bucket_name, object_name, xpires=timedelta(minutes
def get_temp_url(bucket_name, object_name): def get_temp_url(bucket_name, object_name):
# 如果 object_name 为 None 或空字符串,则返回默认图片
if not object_name or not bucket_name:
bucket_name = "system"
object_name = "favicon.ico"
# 使用 presigned_get_object 获取临时 URL
return minio_client.presigned_get_object( return minio_client.presigned_get_object(
bucket_name, object_name, expires=timedelta(seconds=3600) bucket_name, object_name, expires=timedelta(seconds=3600)
) )
def get_temp_url_dict(bucket_name, object_dict, object_name):
# 如果 object_name 为 None 或空字符串,则返回默认图片
if not object_name:
bucket_name = "system"
object_dict = "default"
object_name = "favicon.ico"
# 使用 presigned_get_object 获取临时 URL
return minio_client.presigned_get_object(
bucket_name, object_dict + "/" + object_name, expires=timedelta(seconds=3600)
)
+24
View File
@@ -0,0 +1,24 @@
import redis
class RedisClient:
def __init__(self, config_path="config.yaml"):
self.redis = redis.Redis(
"10.10.12.101",
6379,
0,
decode_responses=True,
)
def set_online(self, device_id: str):
key = f"device:online:{device_id}"
self.redis.set(key, 1)
def set_offline(self, device_id: str):
key = f"device:online:{device_id}"
self.redis.delete(key)
def is_device_online(self, device_id: str) -> bool:
key = f"device:online:{device_id}"
return self.redis.exists(key) == 1
+1
View File
@@ -1,6 +1,7 @@
from .aimessage import * from .aimessage import *
from .aiprofile import * from .aiprofile import *
from .aisession import * from .aisession import *
from .iot import *
from .knowledge import * from .knowledge import *
from .license import * from .license import *
from .report import * from .report import *
+203
View File
@@ -0,0 +1,203 @@
from hashlib import sha256
from config.pgDb import pg_pool
from utils.MyUtils import format_datetime, is_valid_uuid
def get_device_list_db_page(
page: int,
page_size: int,
device_id=None,
name=None,
status=None,
is_superuser=None,
dept_id=None,
startTime=None,
endTime=None,
):
offset = (page - 1) * page_size
conditions = []
params = []
# ---- 设备 IDuuid----
if device_id:
conditions.append("d.id::text LIKE %s")
params.append(f"%{device_id}%")
# ---- 设备名模糊搜索 ----
if name:
conditions.append("d.name LIKE %s")
params.append(f"%{name}%")
# ---- 状态 ----
if status is not None:
conditions.append("d.is_active = %s")
params.append(status == 1)
# ---- 状态 ----
if is_superuser is not None:
conditions.append("d.is_superuser = %s")
params.append(is_superuser == 1)
# ---- 部门 ----
if dept_id and is_valid_uuid(dept_id):
conditions.append("d.dept_id = %s")
params.append(dept_id)
# ---- 时间过滤 ----
if startTime:
conditions.append("d.created_at >= %s")
params.append(startTime)
if endTime:
conditions.append("d.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_users d
{where_clause};
"""
cursor.execute(count_sql, params)
total = cursor.fetchone()[0]
# ---- 分页查询 ----
list_sql = f"""
SELECT
d.id,
d.name,
d.remark,
d.is_active,
d.is_superuser,
d.dept_id,
sd.name AS dept_name,
d.created_at
FROM iot_users d
LEFT JOIN sys_dept sd ON d.dept_id = sd.id
{where_clause}
ORDER BY d.created_at DESC
LIMIT %s OFFSET %s;
"""
cursor.execute(list_sql, params + [page_size, offset])
rows = cursor.fetchall()
result = []
for r in rows:
(
device_id,
name,
remark,
is_active,
is_superuser,
dept_id,
dept_name,
created_at,
) = r
result.append(
{
"id": device_id,
"name": name,
"remark": remark,
"status": 1 if is_active else 0,
"is_superuser": 1 if is_superuser else 0,
"dept_id": dept_id,
"dept_name": dept_name,
"created_at": format_datetime(created_at),
}
)
return result, total
def insert_device(data: dict) -> str:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
name = data.get("name")
salt = name[-4:] if name else ""
password = data.get("password", "123456")
password_hash = sha256((salt + password).encode("utf-8")).hexdigest()
cursor.execute(
"""
INSERT INTO iot_users (name, password_hash, salt, remark, dept_id, is_active, is_superuser)
VALUES (%s, %s, %s, %s, %s, %s, %s)
RETURNING id;
""",
(
name,
password_hash,
salt,
data.get("remark"),
data.get("dept_id"),
bool(data.get("status", 1)),
bool(data.get("is_superuser", 0)),
),
)
device_id = cursor.fetchone()[0]
conn.commit()
return device_id
def update_device_db(id: str, data: dict) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
UPDATE iot_users
SET remark=%s, is_active=%s, dept_id=%s, is_superuser=%s
WHERE id=%s;
""",
(
data.get("remark"),
bool(data.get("status", 1)),
data.get("dept_id"),
bool(data.get("is_superuser", 0)),
id,
),
)
conn.commit()
return cursor.rowcount
def patch_device_db(id: str, data: dict) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
fields = []
params = []
mapping = {
"remark": "remark",
"status": "is_active",
"is_superuser": "is_superuser",
"dept_id": "dept_id",
}
for k, column in mapping.items():
if k in data:
value = (
bool(data[k]) if k in ["status", "is_superuser"] else data[k]
)
fields.append(f"{column} = %s")
params.append(value)
if fields:
sql = f"UPDATE iot_users SET {', '.join(fields)} WHERE id = %s"
params.append(id)
cursor.execute(sql, tuple(params))
conn.commit()
return cursor.rowcount
def delete_device_db(id: str) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM iot_users WHERE id=%s;", (id,))
conn.commit()
return cursor.rowcount
+261
View File
@@ -0,0 +1,261 @@
from config.minIO import get_temp_url_dict
from config.pgDb import pg_pool
from utils.MyUtils import format_datetime
def get_sentinel_record_list_db_page(
page: int,
page_size: int,
record_id=None,
license_plate=None,
vehicle_type=None,
is_inspected=None,
livestock_source=None,
livestock_type=None,
dept_ids=None,
start_time=None,
end_time=None,
):
offset = (page - 1) * page_size
conditions = []
params = []
# ---- 记录 ID ----
if record_id:
conditions.append("r.id::text LIKE %s")
params.append(f"%{record_id}%")
# ---- 车牌号 ----
if license_plate:
conditions.append("r.license_plate LIKE %s")
params.append(f"%{license_plate}%")
# ---- 车型 ----
if vehicle_type:
conditions.append("r.vehicle_type LIKE %s")
params.append(f"%{vehicle_type}%")
# ---- 是否检查 ----
if is_inspected is not None:
conditions.append("r.is_inspected = %s")
params.append(bool(is_inspected))
# ---- 来源 ----
if livestock_source is not None:
conditions.append("r.livestock_source LIKE %s")
params.append(f"%{livestock_source}%")
# ---- 种类 ----
if livestock_type is not None:
conditions.append("r.livestock_type LIKE %s")
params.append(livestock_type)
# ---- 部门 ----
if dept_ids:
conditions.append("r.dept_id = ANY(%s)")
params.append(dept_ids)
# ---- 时间过滤 ----
if start_time:
conditions.append("r.created_at >= %s")
params.append(start_time)
if end_time:
conditions.append("r.created_at <= %s")
params.append(end_time)
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 sentinel_records r
{where_clause};
"""
cursor.execute(count_sql, params)
total = cursor.fetchone()[0]
# ---- 分页查询 ----
list_sql = f"""
SELECT
r.id,
r.license_plate,
r.vehicle_type,
r.license_plate_image,
r.vehicle_image,
r.livestock_type,
r.livestock_source,
r.is_inspected,
r.dept_id,
sd.name AS dept_name,
r.created_at,
r.updated_at,
r.remark
FROM sentinel_records r
LEFT JOIN sys_dept sd ON r.dept_id = sd.id
{where_clause}
ORDER BY r.created_at DESC
LIMIT %s OFFSET %s;
"""
cursor.execute(list_sql, params + [page_size, offset])
rows = cursor.fetchall()
result = []
for r in rows:
(
record_id,
license_plate,
vehicle_type,
license_plate_image,
vehicle_image,
livestock_type,
livestock_source,
is_inspected,
dept_id,
dept_name,
created_at,
updated_at,
remark,
) = r
result.append(
{
"id": record_id,
"license_plate": license_plate,
"vehicle_type": vehicle_type,
"license_plate_image": get_temp_url_dict(
"sentinel", "license_plate", license_plate_image
),
"vehicle_image": get_temp_url_dict(
"sentinel", "vehicle_image", vehicle_image
),
"livestock_type": livestock_type,
"livestock_source": livestock_source,
"is_inspected": 1 if is_inspected else 0,
"dept_id": dept_id,
"dept_name": dept_name,
"created_at": format_datetime(created_at),
"updated_at": format_datetime(updated_at),
"remark": remark,
}
)
return result, total
def insert_sentinel_record(data: dict, dept_id) -> str:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO sentinel_records (
license_plate,
vehicle_type,
license_plate_image,
vehicle_image,
livestock_type,
livestock_source,
is_inspected,
dept_id,
remark,
created_by
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id;
""",
(
data.get("license_plate"),
data.get("vehicle_type"),
data.get("license_plate_image"),
data.get("vehicle_image"),
data.get("livestock_type"),
data.get("livestock_source"),
bool(data.get("is_inspected", False)),
dept_id,
data.get("remark"),
data.get("created_by"),
),
)
record_id = cursor.fetchone()[0]
conn.commit()
return record_id
def update_sentinel_record_db(id: str, data: dict) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
UPDATE sentinel_records
SET
license_plate = %s,
vehicle_type = %s,
license_plate_image = %s,
vehicle_image = %s,
livestock_type = %s,
livestock_source = %s,
is_inspected = %s,
remark = %s,
updated_at = now()
WHERE id = %s;
""",
(
data.get("license_plate"),
data.get("vehicle_type"),
data.get("license_plate_image"),
data.get("vehicle_image"),
data.get("livestock_type"),
data.get("livestock_source"),
bool(data.get("is_inspected", False)),
data.get("remark"),
id,
),
)
conn.commit()
return cursor.rowcount
def patch_sentinel_record_db(id: str, data: dict) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
fields = []
params = []
mapping = {
"license_plate": "license_plate",
"vehicle_type": "vehicle_type",
"license_plate_image": "license_plate_image",
"vehicle_image": "vehicle_image",
"livestock_type": "livestock_type",
"livestock_source": "livestock_source",
"is_inspected": "is_inspected",
"remark": "remark",
}
for k, column in mapping.items():
if k in data:
value = data[k]
# 如果字段是 "is_inspected",将其转换为布尔类型
if k == "is_inspected":
value = bool(value) # 转换为布尔值
fields.append(f"{column} = %s")
params.append(value)
if fields:
sql = f"UPDATE sentinel_records SET {', '.join(fields)} WHERE id = %s"
params.append(id)
cursor.execute(sql, tuple(params))
conn.commit()
return cursor.rowcount
def delete_sentinel_record_db(id: str) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM sentinel_records WHERE id=%s;", (id,))
conn.commit()
return cursor.rowcount
+303 -50
View File
@@ -2,6 +2,7 @@ import json
from uuid import UUID from uuid import UUID
from config.pgDb import pg_pool from config.pgDb import pg_pool
from utils import MyUtils
from utils.MyUtils import format_datetime, is_valid_uuid from utils.MyUtils import format_datetime, is_valid_uuid
@@ -387,7 +388,7 @@ def get_role_list_db_page(page: int, page_size: int, rid=None, name=None, remark
"id": role_id, "id": role_id,
"name": name, "name": name,
"remark": remark, "remark": remark,
"created_at": created_at, "created_at": format_datetime(created_at),
"permissions": menu_ids or [], "permissions": menu_ids or [],
} }
) )
@@ -411,9 +412,9 @@ def get_user_list_db_page(
params = [] params = []
# ---- 用户 ID,必须是 UUID,否则忽略 ---- # ---- 用户 ID,必须是 UUID,否则忽略 ----
if uid and is_valid_uuid(uid): if uid:
conditions.append("u.id = %s") conditions.append("u.id::text LIKE %s")
params.append(uid) params.append(f"%{uid}%")
# ---- 用户名模糊搜索 ---- # ---- 用户名模糊搜索 ----
if username: if username:
@@ -506,19 +507,11 @@ def get_user_list_db_page(
return result, total return result, total
# 检查用户名是否存在 # 检查登录账号是否存在
def db_user_name_exists(username: str, id: str | None = None) -> bool: def db_user_name_exists(username: str) -> bool:
with pg_pool.getConn() as conn: with pg_pool.getConn() as conn:
with conn.cursor() as cursor: with conn.cursor() as cursor:
if id: cursor.execute("SELECT COUNT(*) FROM users WHERE email = %s;", (username,))
cursor.execute(
"SELECT COUNT(*) FROM users WHERE username = %s AND id <> %s;",
(username, id),
)
else:
cursor.execute(
"SELECT COUNT(*) FROM users WHERE username = %s;", (username,)
)
return cursor.fetchone()[0] > 0 return cursor.fetchone()[0] > 0
@@ -554,40 +547,6 @@ def insert_user(data: dict) -> str:
return user_id return user_id
# 更新用户
def update_user_db(id: str, data: dict) -> int:
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
UPDATE users
SET username=%s, email=%s, phone=%s, is_active=%s, dept_id=%s
WHERE id=%s;
""",
(
data.get("name"),
data.get("email"),
data.get("phone"),
bool(data.get("status", 1)),
data.get("dept_id"),
id,
),
)
# 更新用户角色关系:先删除旧的,再插入新的
cursor.execute("DELETE FROM sys_user_role WHERE user_id=%s;", (id,))
role_ids = data.get("roles", [])
if role_ids:
for role_id in role_ids:
cursor.execute(
"INSERT INTO sys_user_role (user_id, role_id) VALUES (%s, %s);",
(id, role_id),
)
conn.commit()
return cursor.rowcount
# 局部更新用户(PATCH # 局部更新用户(PATCH
def patch_user_db(id: str, data: dict) -> int: def patch_user_db(id: str, data: dict) -> int:
with pg_pool.getConn() as conn: with pg_pool.getConn() as conn:
@@ -603,6 +562,7 @@ def patch_user_db(id: str, data: dict) -> int:
"name": "username", "name": "username",
"email": "email", "email": "email",
"phone": "phone", "phone": "phone",
"password_hash": "password_hash",
"status": "is_active", "status": "is_active",
"dept_id": "dept_id", "dept_id": "dept_id",
} }
@@ -676,7 +636,7 @@ def get_menus_by_ids(menu_ids: list, plat_id: int):
SELECT id, pid, name, path, component, redirect, SELECT id, pid, name, path, component, redirect,
auth_code, type, meta, created_at, updated_at auth_code, type, meta, created_at, updated_at
FROM sys_menu FROM sys_menu
WHERE id = ANY(%s::varchar[]) AND plat_id = %s WHERE id = ANY(%s::varchar[]) AND plat_id = %s AND type != 'button'
ORDER BY created_at ASC; ORDER BY created_at ASC;
""" """
# 转换 uuid 列表为 str 列表 # 转换 uuid 列表为 str 列表
@@ -697,3 +657,296 @@ def build_menu_tree(items):
else: else:
tree.append(item) tree.append(item)
return tree return tree
def get_dict_list(keyword="", page=1, page_size=10):
"""
获取系统字典列表,支持分页和关键字搜索(key / name)
"""
offset = (page - 1) * page_size
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
# 1️⃣ 查询总条数
cursor.execute(
"""
SELECT COUNT(*)
FROM sys_dict
WHERE (
%s = ''
OR key ILIKE '%%' || %s || '%%'
OR name ILIKE '%%' || %s || '%%'
)
""",
(keyword, keyword, keyword),
)
total = cursor.fetchone()[0]
# 2️⃣ 查询当前页数据
cursor.execute(
"""
SELECT id, key, name, remark, created_at
FROM sys_dict
WHERE (
%s = ''
OR key ILIKE '%%' || %s || '%%'
OR name ILIKE '%%' || %s || '%%'
)
ORDER BY created_at DESC
LIMIT %s OFFSET %s
""",
(keyword, keyword, keyword, page_size, offset),
)
rows = cursor.fetchall()
result = []
for row in rows:
result.append(
{
"id": row[0],
"key": row[1],
"name": row[2],
"remark": row[3],
"created_at": MyUtils.format_datetime(row[4]),
}
)
return total, result
def db_create_dict(key: str, name: str, remark: str):
"""
在数据库中创建字典
"""
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO sys_dict (id, key, name, remark, created_at)
VALUES (gen_random_uuid(), %s, %s, %s, now())
RETURNING id
""",
(key, name, remark),
)
new_id = cursor.fetchone()[0]
return new_id
def db_update_dict(id: str, key: str, name: str, remark: str):
"""
在数据库中更新字典
"""
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
UPDATE sys_dict
SET key=%s, name=%s, remark=%s
WHERE id=%s
""",
(key, name, remark, id),
)
return id
def db_delete_dict(id: str):
"""
在数据库中删除字典
"""
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM sys_dict WHERE id=%s", (id,))
return id
def db_get_dict_detail(dict_id: str):
"""
获取字典详情列表
"""
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT id, value, sort, pid, dict_id, remark, created_at, updated_at
FROM sys_dict_detail
WHERE dict_id=%s
ORDER BY sort ASC, created_at ASC
""",
(dict_id,),
)
rows = cursor.fetchall()
result = []
for row in rows:
result.append(
{
"id": row[0],
"value": row[1],
"sort": row[2],
"pid": row[3],
"dict_id": row[4],
"remark": row[5],
"created_at": row[6].isoformat() if row[6] else None,
"updated_at": row[7].isoformat() if row[7] else None,
}
)
return result
def db_create_dict_detail(
value: str, dict_id: str, sort: int = 0, pid: str = None, remark: str = None
):
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
INSERT INTO sys_dict_detail (id, value, dict_id, sort, pid, remark, created_at)
VALUES (gen_random_uuid(), %s, %s, %s, %s, %s, now())
RETURNING id
""",
(value, dict_id, sort, pid, remark),
)
new_id = cursor.fetchone()[0]
return new_id
def db_update_dict_detail(
id: str, value: str = None, sort: int = None, pid: str = None, remark: str = None
):
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
UPDATE sys_dict_detail
SET value=COALESCE(%s, value),
sort=COALESCE(%s, sort),
pid=COALESCE(%s, pid),
remark=COALESCE(%s, remark),
updated_at=now()
WHERE id=%s
""",
(value, sort, pid, remark, id),
)
return id
def db_delete_dict_detail(id: str):
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM sys_dict_detail WHERE id=%s", (id,))
return id
def get_dict_detail_list_by_key(dict_key: str):
"""
通过字典 key 获取字典明细列表
"""
with pg_pool.getConn() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT
d.id,
d.value,
d.sort,
d.pid,
d.remark,
d.created_at
FROM sys_dict_detail d
JOIN sys_dict s ON d.dict_id = s.id
WHERE s.key = %s
ORDER BY d.sort ASC, d.created_at ASC
""",
(dict_key,),
)
rows = cursor.fetchall()
result = []
for row in rows:
result.append(
{
"id": row[0],
"value": row[1],
"sort": row[2],
"pid": row[3],
"remark": row[4],
"created_at": MyUtils.format_datetime(row[5]),
}
)
return result
def get_dept_ids_by_user_id(user_id: UUID) -> list:
# 第一步:通过 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()
if not dept_id:
raise ValueError(
f"User with id {user_id} not found or has no department."
)
dept_id = dept_id[0]
# 第二步:通过 dept_id 获取所有下级部门的 dept_id
cursor.execute(
"""
WITH RECURSIVE dept_hierarchy AS (
SELECT id FROM sys_dept WHERE id = %s
UNION
SELECT d.id FROM sys_dept d
INNER JOIN dept_hierarchy dh ON d.parent_id = dh.id
)
SELECT id FROM dept_hierarchy;
""",
(dept_id,),
)
dept_ids = [row[0] for row in cursor.fetchall()]
return dept_ids
def get_dept_ids_by_user_id(user_id: UUID) -> list:
# 第一步:通过 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()
if not dept_id:
raise ValueError(
f"User with id {user_id} not found or has no department."
)
dept_id = dept_id[0]
# 第二步:通过 dept_id 获取所有下级部门的 dept_id
cursor.execute(
"""
WITH RECURSIVE dept_hierarchy AS (
SELECT id FROM sys_dept WHERE id = %s
UNION
SELECT d.id FROM sys_dept d
INNER JOIN dept_hierarchy dh ON d.parent_id = dh.id
)
SELECT id FROM dept_hierarchy;
""",
(dept_id,),
)
dept_ids = [row[0] for row in cursor.fetchall()]
return dept_ids
def get_dept_id_by_user_id(user_id: UUID) -> list:
# 第一步:通过 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 dept_id
+25
View File
@@ -0,0 +1,25 @@
import asyncio
from typing import List
from fastapi import WebSocket
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
self.lock = asyncio.Lock()
async def connect(self, websocket: WebSocket):
await websocket.accept()
async with self.lock:
self.active_connections.append(websocket)
async def disconnect(self, websocket: WebSocket):
async with self.lock:
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def broadcast(self, message: dict):
async with self.lock:
for ws in self.active_connections:
await ws.send_json(message)
+10
View File
@@ -0,0 +1,10 @@
from typing import Optional
from pydantic import BaseModel
class EMQXWebhook(BaseModel):
event: str
clientid: str
username: Optional[str] = None
timestamp: Optional[int] = None
+1 -1
View File
@@ -22,7 +22,7 @@ Pillow==11.3.0
python-multipart==0.0.20 python-multipart==0.0.20
aio_pika==9.5.7 aio_pika==9.5.7
ultralytics==8.3.227 ultralytics==8.3.227
redis==7.1.0
# MCP服务 # MCP服务
python-dotenv>=1.0.0 python-dotenv>=1.0.0
websockets>=11.0.3 websockets>=11.0.3
+123
View File
@@ -0,0 +1,123 @@
from uuid import UUID
from fastapi import APIRouter
from fastapi import Depends
from config.redis import RedisClient
from db.postgres.iot import *
from models.BaseResponse import BaseResponse
from models.EMQXWebhook import EMQXWebhook
from routers.WS import ws_manager
iot_router = APIRouter()
redis_client = RedisClient()
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)
await ws_manager.broadcast({"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})
print(f"[OFFLINE] {device_id}")
else:
# 其他事件直接忽略
print(f"[IGNORE] {event}")
return {"ok": True}
@iot_router.get("/common/device/list")
async def get_device_list(
page: int = 1,
pageSize: int = 10,
id: str | None = None,
name: str | None = None,
status: int | None = None,
is_superuser: int | 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"}
devices, total = get_device_list_db_page(
page, pageSize, id, name, status, is_superuser, dept_id, startTime, endTime
)
# ===== 👇 核心:补在线状态 =====
for d in devices:
device_id = d["name"] # 账号
d["online"] = redis_client.is_device_online(device_id) == 1
return BaseResponse(data={"list": devices, "total": total})
@iot_router.post("/common/device")
async def create_device(data: dict, user_id: UUID = Depends(get_user_id_from_token)):
if not user_id:
return {"error": "userId is required"}
new_id = insert_device(data)
return BaseResponse(data={"id": new_id})
@iot_router.put("/common/device/{id}")
async def update_device(
id: str,
data: dict,
user_id: UUID = Depends(get_user_id_from_token),
):
if not user_id:
return {"error": "userId is required"}
count = update_device_db(id, data)
if count == 0:
return BaseResponse(status=False, message="设备不存在", data=None)
return BaseResponse(data=True)
@iot_router.patch("/common/device/{id}")
async def patch_device(
id: str,
data: dict,
user_id: UUID = Depends(get_user_id_from_token),
):
if not user_id:
return {"error": "userId is required"}
count = patch_device_db(id, data)
if count == 0:
return BaseResponse(status=False, message="设备不存在", data=None)
return BaseResponse(data=True)
@iot_router.delete("/common/device/{id}")
async def delete_device(
id: str,
user_id: UUID = Depends(get_user_id_from_token),
):
if not user_id:
return {"error": "userId is required"}
deleted = delete_device_db(id)
if deleted == 0:
return BaseResponse(status=False, message="设备不存在", data=None)
return BaseResponse(data=True)
+98
View File
@@ -0,0 +1,98 @@
from uuid import UUID
from fastapi import Depends, APIRouter
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 *
from models.BaseResponse import BaseResponse
# -------------------- 设备接口 --------------------
sentinel_router = APIRouter()
@sentinel_router.get("/record/list")
async def get_sentinel_record_list(
page: int = 1,
page_size: int = 10,
id: str | None = None,
license_plate: str | None = None,
vehicle_type: str | None = None,
is_inspected: int | None = None,
livestock_type: str = None,
livestock_source: str | None = None,
start_time: str | None = None,
end_time: str | None = None,
user_id: UUID = Depends(get_user_id_from_token),
):
if not user_id:
return {"error": "userId is required"}
dept_ids = get_dept_ids_by_user_id(user_id)
print(dept_ids) # 输出所有部门的 dept_id 列表
records, total = get_sentinel_record_list_db_page(
page,
page_size,
id,
license_plate,
vehicle_type,
is_inspected,
livestock_source,
livestock_type,
dept_ids,
start_time,
end_time,
)
return BaseResponse(data={"list": records, "total": total})
@sentinel_router.post("/record")
async def create_sentinel_record(
data: dict, user_id: UUID = Depends(get_user_id_from_token)
):
if not user_id:
return {"error": "userId is required"}
dept_id = get_dept_id_by_user_id(user_id)
new_id = insert_sentinel_record(data, dept_id)
return BaseResponse(data={"id": new_id})
@sentinel_router.put("/record/{id}")
async def update_sentinel_record(
id: str, data: dict, user_id: UUID = Depends(get_user_id_from_token)
):
if not user_id:
return {"error": "userId is required"}
count = update_sentinel_record_db(id, data)
if count == 0:
return BaseResponse(status=False, message="记录不存在", data=None)
return BaseResponse(data=True)
@sentinel_router.patch("/record/{id}")
async def patch_sentinel_record(
id: str, data: dict, user_id: UUID = Depends(get_user_id_from_token)
):
if not user_id:
return {"error": "userId is required"}
count = patch_sentinel_record_db(id, data)
if count == 0:
return BaseResponse(status=False, message="记录不存在", data=None)
return BaseResponse(data=True)
@sentinel_router.delete("/record/{id}")
async def delete_sentinel_record(
id: str, user_id: UUID = Depends(get_user_id_from_token)
):
if not user_id:
return {"error": "userId is required"}
deleted = delete_sentinel_record_db(id)
if deleted == 0:
return BaseResponse(status=False, message="记录不存在", data=None)
return BaseResponse(data=True)
+118 -19
View File
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, Query
from config.security import get_user_id_from_token from config.security import get_user_id_from_token
from db.postgres.system import * from db.postgres.system import *
@@ -60,7 +60,7 @@ async def dept_update(
parent_id = data.get("pid") parent_id = data.get("pid")
name = data.get("name") name = data.get("name")
comment = data.get("comment") comment = data.get("remark")
rowcount = update_dept(id, parent_id, name, comment) rowcount = update_dept(id, parent_id, name, comment)
if rowcount == 0: if rowcount == 0:
@@ -294,12 +294,11 @@ async def delete_role(
@systemRouter.get("/user/name-exists") @systemRouter.get("/user/name-exists")
async def user_name_exists( async def user_name_exists(
username: str, username: str,
id: str | None = None,
user_id: UUID = Depends(get_user_id_from_token), 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"}
exists = db_user_name_exists(username, id) exists = db_user_name_exists(username)
return BaseResponse(data=exists) return BaseResponse(data=exists)
@@ -333,21 +332,6 @@ async def create_user(data: dict, user_id: UUID = Depends(get_user_id_from_token
return BaseResponse(data={"id": new_id}) return BaseResponse(data={"id": new_id})
@systemRouter.put("/user/{id}")
async def update_user(
id: str,
data: dict,
user_id: UUID = Depends(get_user_id_from_token),
):
if not user_id:
return {"error": "userId is required"}
count = update_user_db(id, data)
if count == 0:
return BaseResponse(status=False, message="用户不存在", data=None)
return BaseResponse(data=True)
@systemRouter.patch("/user/{id}") @systemRouter.patch("/user/{id}")
async def patch_user( async def patch_user(
id: str, id: str,
@@ -375,3 +359,118 @@ async def delete_user(
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)
@systemRouter.get("/dict/list")
def getScVideoList(
user_id: UUID = Depends(get_user_id_from_token),
name: str = "",
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
):
if not user_id:
return {"error": "userId is required"}
total, items = get_dict_list(name, page=page, page_size=page_size)
return BaseResponse(
data={
"total": total,
"items": items,
}
)
@systemRouter.post("/dict")
def create_dict_api(data: dict, user_id: UUID = Depends(get_user_id_from_token)):
"""
创建字典接口
data = { "key": str, "name": str, "remark": str }
"""
if not user_id:
return {"error": "userId is required"}
new_id = db_create_dict(data.get("key"), data.get("name"), data.get("remark"))
return BaseResponse(data={"id": new_id})
@systemRouter.put("/dict/{id}")
def update_dict_api(
id: str, data: dict, user_id: UUID = Depends(get_user_id_from_token)
):
"""
更新字典接口
"""
if not user_id:
return {"error": "userId is required"}
db_update_dict(id, data.get("key"), data.get("name"), data.get("remark"))
return BaseResponse(data={"id": id})
@systemRouter.delete("/dict/{id}")
def delete_dict_api(id: str, user_id: UUID = Depends(get_user_id_from_token)):
"""
删除字典接口
"""
if not user_id:
return {"error": "userId is required"}
db_delete_dict(id)
return BaseResponse(data={"id": id})
@systemRouter.get("/dict/detail/list")
def get_dict_detail_api(dictId: str, user_id: UUID = Depends(get_user_id_from_token)):
if not user_id:
return {"error": "userId is required"}
items = db_get_dict_detail(dictId)
return BaseResponse(data={"list": items})
@systemRouter.post("/dict/detail")
def create_dict_detail_api(data: dict, user_id: UUID = Depends(get_user_id_from_token)):
if not user_id:
return {"error": "userId is required"}
new_id = db_create_dict_detail(
value=data.get("value"),
dict_id=data.get("dict_id"),
sort=data.get("sort", 0),
pid=data.get("pid"),
remark=data.get("remark"),
)
return BaseResponse(data={"id": new_id})
@systemRouter.put("/dict/detail/{id}")
def update_dict_detail_api(
id: str, data: dict, user_id: UUID = Depends(get_user_id_from_token)
):
if not user_id:
return {"error": "userId is required"}
db_update_dict_detail(
id=id,
value=data.get("value"),
sort=data.get("sort"),
pid=data.get("pid"),
remark=data.get("remark"),
)
return BaseResponse(data={"id": id})
@systemRouter.delete("/dict/detail/{id}")
def delete_dict_detail_api(id: str, user_id: UUID = Depends(get_user_id_from_token)):
if not user_id:
return {"error": "userId is required"}
db_delete_dict_detail(id)
return BaseResponse(data={"id": id})
@systemRouter.get("/dict/getValue")
def get_dict_detail_by_key(
key: str = Query(..., min_length=1),
):
if not key:
return {"error": "key is required"}
items = get_dict_detail_list_by_key(key)
return BaseResponse(data=items)
+23
View File
@@ -0,0 +1,23 @@
from fastapi import APIRouter
from starlette.websockets import WebSocket, WebSocketDisconnect
from db.postgres.ws_manager import ConnectionManager
ws_manager = ConnectionManager()
iot_ws_router = APIRouter()
@iot_ws_router.websocket("/device-status")
async def websocket_device_status(websocket: WebSocket):
await ws_manager.connect(websocket)
print("[WS] client connected")
try:
while True:
# 这里不需要接收任何消息
await websocket.receive_text()
except WebSocketDisconnect:
await ws_manager.disconnect(websocket)
print("[WS] client disconnected")
+3 -4
View File
@@ -8,11 +8,10 @@ import json
import time import time
import uuid import uuid
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
from websockets.server import WebSocketServerProtocol
from websockets.exceptions import ConnectionClosed
from utils.logger import get_logger
logger = get_logger() from fastapi import logger
from websockets.exceptions import ConnectionClosed
from websockets.legacy.server import WebSocketServerProtocol
class RobotConnection: class RobotConnection: