diff --git a/backend/app/core/middleware.py b/backend/app/core/middleware.py new file mode 100644 index 0000000..a987219 --- /dev/null +++ b/backend/app/core/middleware.py @@ -0,0 +1,81 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from fastapi import Request, Response +import time +from app.services.terminal_service import terminal_service +from app.services.jwt_service import jwt_service +from app.core.response import create_api_response + +class TerminalCheckMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + # 1. 检查是否有 Imei 头,没有则认为是普通请求,直接放行 + imei = request.headers.get("Imei") + if not imei: + return await call_next(request) + + # 2. 检查时间戳 (防重放/时钟同步) + # 优先从Header获取,如果没有则尝试从Query Parameter获取 + client_time_str = request.headers.get("time") or request.query_params.get("time") + + if client_time_str: + try: + client_time = int(client_time_str) + server_time = int(time.time() * 1000) + + # 允许 10 分钟的误差 (10 * 60 * 1000 = 600000 ms) + # 考虑到网络延迟和设备时间未校准,设置宽松一点 + if abs(server_time - client_time) > 600000: + return create_api_response( + code="400", + message="设备时间与服务器时间差距过大,请校准时间" + ) + except ValueError: + # 时间格式错误,暂时忽略或返回错误 + pass + + # 3. 提取其他设备信息 + device_type = request.headers.get("deviceType", "UNKNOWN") + # device_info 可能是 "UNIS iMeeting a7" + device_info = request.headers.get("deviceInfo", "Unknown Device") + + # 获取客户端IP (考虑代理) + client_ip = request.client.host + if "x-forwarded-for" in request.headers: + client_ip = request.headers["x-forwarded-for"].split(",")[0].strip() + elif "x-real-ip" in request.headers: + client_ip = request.headers["x-real-ip"] + + # 获取当前用户ID (如果已登录) + user_id = None + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + try: + token = auth_header.split(" ")[1] + payload = jwt_service.verify_token(token) + if payload: + user_id = payload.get("user_id") + except Exception: + pass # 忽略token解析错误,只记录设备在线状态 + + # 4. 调用服务进行检查和更新 + # 注意:这里是同步调用数据库,但在 async 中可能会阻塞 loop + # 理想情况下 terminal_service 应该是 async 的,或者使用 run_in_executor + # 但由于数据库操作较快,且 mysql-connector 是同步的,暂时直接调用 + # 如果并发高,建议将 service 改为 async + + result = terminal_service.check_and_update_terminal( + imei=imei, + terminal_type=device_type, + terminal_name=device_info, + ip_address=client_ip, + user_id=user_id + ) + + if not result["allowed"]: + return create_api_response( + code="403", + message=result["reason"] + ) + + # 5. 放行请求 + response = await call_next(request) + return response diff --git a/backend/app/main.py b/backend/app/main.py index 6057481..c2d9333 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -14,6 +14,7 @@ from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.openapi.docs import get_swagger_ui_html +from app.core.middleware import TerminalCheckMiddleware from app.api.endpoints import auth, users, meetings, tags, admin, admin_dashboard, tasks, prompts, knowledge_base, client_downloads, voiceprint, audio, dict_data, hot_words, external_apps, terminals from app.core.config import UPLOAD_DIR, API_CONFIG @@ -25,6 +26,9 @@ app = FastAPI( redoc_url=None ) +# 添加终端检查中间件 (在CORS之前添加,以便位于CORS内部) +app.add_middleware(TerminalCheckMiddleware) + # 添加CORS中间件 app.add_middleware( CORSMiddleware, diff --git a/backend/app/services/terminal_service.py b/backend/app/services/terminal_service.py index 00656be..9375310 100644 --- a/backend/app/services/terminal_service.py +++ b/backend/app/services/terminal_service.py @@ -45,9 +45,12 @@ class TerminalService: SELECT t.*, u.username as creator_username, + cu.username as current_username, + cu.caption as current_user_caption, dd.label_cn as terminal_type_name FROM terminals t LEFT JOIN users u ON t.created_by = u.user_id + LEFT JOIN users cu ON t.current_user_id = cu.user_id LEFT JOIN dict_data dd ON t.terminal_type = dd.dict_code AND dd.dict_type = 'terminal_type' WHERE {where_clause} ORDER BY t.created_at DESC @@ -188,4 +191,61 @@ class TerminalService: cursor.close() return updated + def check_and_update_terminal(self, imei: str, terminal_type: str, terminal_name: str, ip_address: str, user_id: Optional[int] = None) -> dict: + """ + 检查并更新终端状态(中间件调用) + """ + try: + with get_db_connection() as conn: + cursor = conn.cursor(dictionary=True) + + # 检查是否存在 + cursor.execute("SELECT id, status, is_activated FROM terminals WHERE imei = %s", (imei,)) + existing = cursor.fetchone() + + current_time = datetime.datetime.now() + + if existing: + # 检查是否被停用 + if existing['status'] == 0: + return {"allowed": False, "reason": "设备已被停用", "terminal_id": existing['id']} + + # 更新在线时间、IP、名称(如果变了)、当前用户 + update_query = """ + UPDATE terminals + SET last_online_at = %s, + ip_address = %s, + current_user_id = %s, + # 如果设备没名字,尝试用上报的名字填充 + terminal_name = IF(terminal_name IS NULL OR terminal_name = '', %s, terminal_name), + # 如果设备没类型,尝试用上报的类型填充 + terminal_type = IF(terminal_type IS NULL OR terminal_type = '', %s, terminal_type) + WHERE id = %s + """ + cursor.execute(update_query, (current_time, ip_address, user_id, terminal_name, terminal_type, existing['id'])) + conn.commit() + + return {"allowed": True, "reason": "", "terminal_id": existing['id']} + else: + # 新设备自动注册并激活 + insert_query = """ + INSERT INTO terminals ( + imei, terminal_name, terminal_type, status, + is_activated, activated_at, last_online_at, ip_address, created_at, current_user_id + ) VALUES (%s, %s, %s, 1, 1, %s, %s, %s, %s, %s) + """ + cursor.execute(insert_query, ( + imei, terminal_name, terminal_type, + current_time, current_time, ip_address, current_time, user_id + )) + new_id = cursor.lastrowid + conn.commit() + + return {"allowed": True, "reason": "", "terminal_id": new_id} + + except Exception as e: + print(f"Error in check_and_update_terminal: {e}") + # 数据库错误暂时放行,或者根据安全策略决定 + return {"allowed": True, "reason": f"DB Error: {str(e)}", "terminal_id": None} + terminal_service = TerminalService() diff --git a/backend/sql/migrations/add_current_user_to_terminals.sql b/backend/sql/migrations/add_current_user_to_terminals.sql new file mode 100644 index 0000000..06ea345 --- /dev/null +++ b/backend/sql/migrations/add_current_user_to_terminals.sql @@ -0,0 +1,4 @@ +-- 为terminals表添加当前绑定用户ID字段 +ALTER TABLE `terminals` +ADD COLUMN `current_user_id` INT DEFAULT NULL COMMENT '当前绑定/使用的用户ID', +ADD CONSTRAINT `fk_terminals_current_user` FOREIGN KEY (`current_user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL; diff --git a/frontend/src/pages/admin/TerminalManagement.css b/frontend/src/pages/admin/TerminalManagement.css index 1e1b213..50ad6e2 100644 --- a/frontend/src/pages/admin/TerminalManagement.css +++ b/frontend/src/pages/admin/TerminalManagement.css @@ -215,6 +215,13 @@ vertical-align: middle; } +.truncate-cell { + max-width: 180px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .data-table tr:last-child td { border-bottom: none; } diff --git a/frontend/src/pages/admin/TerminalManagement.jsx b/frontend/src/pages/admin/TerminalManagement.jsx index b34ea10..a3dbaed 100644 --- a/frontend/src/pages/admin/TerminalManagement.jsx +++ b/frontend/src/pages/admin/TerminalManagement.jsx @@ -283,6 +283,7 @@ const TerminalManagement = () => {